diff --git a/shared/index.tsx b/shared/index.tsx index fd81542e..4c9b7d59 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -110,6 +110,7 @@ export { loginWithToken, registerWithEmail, createTemporaryUser, + claimTemporaryUser, searchUserWithUniqueName, checkTokenValid, modifyUserField, @@ -136,7 +137,7 @@ export type { AppStore, AppState, AppDispatch } from './redux/store'; // utils export { joinArray } from './utils/array-helper'; -export { SYSTEM_USERID } from './utils/consts'; +export { NAME_REGEXP, SYSTEM_USERID } from './utils/consts'; export { shouldShowMessageTime, getMessageTimeDiff, diff --git a/shared/model/user.ts b/shared/model/user.ts index 6b14f9df..b62341b4 100644 --- a/shared/model/user.ts +++ b/shared/model/user.ts @@ -7,6 +7,7 @@ export interface UserBaseInfo { nickname: string; discriminator: string; avatar: string | null; + temporary: boolean; } export interface UserLoginInfo extends UserBaseInfo { @@ -82,6 +83,23 @@ export async function createTemporaryUser( return data; } +/** + * 认领访客账号 + */ +export async function claimTemporaryUser( + userId: string, + email: string, + password: string +): Promise { + const { data } = await request.post('/api/user/claimTemporaryUser', { + userId, + email, + password, + }); + + return data; +} + /** * 使用唯一标识名搜索用户 * @param uniqueName 唯一标识用户昵称: 用户昵称#0000 diff --git a/shared/utils/consts.ts b/shared/utils/consts.ts index 9342f0f7..dedfbee6 100644 --- a/shared/utils/consts.ts +++ b/shared/utils/consts.ts @@ -1,3 +1,11 @@ +/** + * 昵称合法性匹配 + * 最大八个汉字内容或者16字英文 + * 且中间不能有空格 + */ +export const NAME_REGEXP = + /^([0-9a-zA-Z]{1,2}|[\u4e00-\u9eff]|[\u3040-\u309Fー]|[\u30A0-\u30FF]){1,8}$/; + /** * 系统语言的常量 */ diff --git a/web/src/components/GlobalTemporaryTip.tsx b/web/src/components/GlobalTemporaryTip.tsx new file mode 100644 index 00000000..cf2ba85b --- /dev/null +++ b/web/src/components/GlobalTemporaryTip.tsx @@ -0,0 +1,45 @@ +import { openModal } from '@/plugin/common'; +import { Button } from 'antd'; +import React, { useCallback } from 'react'; +import { t, Trans, useUserInfo } from 'tailchat-shared'; +import { closeModal } from './Modal'; +import { ClaimTemporaryUser } from './modals/ClaimTemporaryUser'; + +/** + * 访客账号提示 + */ +export const GlobalTemporaryTip: React.FC = React.memo(() => { + const userInfo = useUserInfo(); + const show = userInfo?.temporary === true; + + const handleClaim = useCallback(() => { + if (!userInfo?._id) { + return; + } + + const key = openModal( + closeModal(key)} + /> + ); + }, [userInfo?._id]); + + return show ? ( +
+ + 当前使用的是一个临时账号,{' '} + + + {t('')} +
+ ) : null; +}); +GlobalTemporaryTip.displayName = 'GlobalTemporaryTip'; diff --git a/web/src/components/modals/ClaimTemporaryUser.tsx b/web/src/components/modals/ClaimTemporaryUser.tsx new file mode 100644 index 00000000..e7435f38 --- /dev/null +++ b/web/src/components/modals/ClaimTemporaryUser.tsx @@ -0,0 +1,76 @@ +import { setUserJWT } from '@/utils/jwt-helper'; +import { setGlobalUserLoginInfo } from '@/utils/user-helper'; +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + claimTemporaryUser, + createFastFormSchema, + FastFormFieldMeta, + fieldSchema, + t, + useAsyncRequest, + userActions, +} from 'tailchat-shared'; +import { ModalWrapper } from '../Modal'; +import { WebFastForm } from '../WebFastForm'; + +interface Values { + email: string; + password: string; + [key: string]: unknown; +} + +const fields: FastFormFieldMeta[] = [ + { type: 'text', name: 'email', label: t('邮箱') }, + { + type: 'password', + name: 'password', + label: t('密码'), + }, +]; + +const schema = createFastFormSchema({ + email: fieldSchema + .string() + .required(t('邮箱不能为空')) + .email(t('邮箱格式不正确')), + password: fieldSchema + .string() + .min(6, t('密码不能低于6位')) + .required(t('密码不能为空')), +}); + +interface ClaimTemporaryUserProps { + userId: string; + onSuccess: () => void; +} +export const ClaimTemporaryUser: React.FC = React.memo( + (props) => { + const userId = props.userId; + const dispatch = useDispatch(); + + const [{}, handleClaim] = useAsyncRequest( + async (values: Values) => { + const data = await claimTemporaryUser( + userId, + values.email, + values.password + ); + + setGlobalUserLoginInfo(data); + await setUserJWT(data.token); + dispatch(userActions.setUserInfo(data)); + + typeof props.onSuccess === 'function' && props.onSuccess(); + }, + [, userId, props.onSuccess] + ); + + return ( + + + + ); + } +); +ClaimTemporaryUser.displayName = 'ClaimTemporaryUser'; diff --git a/web/src/routes/Entry/GuestView.tsx b/web/src/routes/Entry/GuestView.tsx index 07f14c5c..740d7511 100644 --- a/web/src/routes/Entry/GuestView.tsx +++ b/web/src/routes/Entry/GuestView.tsx @@ -21,7 +21,7 @@ export const GuestView: React.FC = React.memo(() => { const [nickname, setNickname] = useState(''); const [{ loading }, handleCreateTemporaryUser] = useAsyncRequest(async () => { - await string().required(t('昵称不能为空')).validate(nickname); + await string().required(t('昵称不能为空')).max(16).validate(nickname); const data = await createTemporaryUser(nickname); diff --git a/web/src/routes/Main/index.tsx b/web/src/routes/Main/index.tsx index 38671e0c..4413fdc0 100644 --- a/web/src/routes/Main/index.tsx +++ b/web/src/routes/Main/index.tsx @@ -1,3 +1,4 @@ +import { GlobalTemporaryTip } from '@/components/GlobalTemporaryTip'; import { useRecordMeasure } from '@/utils/measure-helper'; import React from 'react'; import { MainContent } from './Content'; @@ -10,13 +11,17 @@ const MainRoute: React.FC = React.memo(() => { useShortcuts(); return ( -
- - + +
+ - - -
+
+ + + +
+
+ ); }); MainRoute.displayName = 'MainRoute';