feat: 注册限流配置

feat: 页面加载动画
feat: md样式优化
feat: 移动端全屏覆盖
This commit is contained in:
archer
2023-03-04 13:30:20 +08:00
parent 0ecf576e4e
commit 2cc32d1806
28 changed files with 515 additions and 252 deletions

View File

@@ -1,11 +1,20 @@
import type { AppProps, NextWebVitalsMetric } from 'next/app';
import Script from 'next/script';
import Head from 'next/head';
import { ChakraProvider } from '@chakra-ui/react';
import Layout from '@/components/Layout';
import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router';
import 'nprogress/nprogress.css';
import '../styles/reset.scss';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
export default function App({ Component, pageProps }: AppProps) {
// Create a client
const queryClient = new QueryClient({
@@ -28,7 +37,6 @@ export default function App({ Component, pageProps }: AppProps) {
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
/>
<link rel="icon" href="/favicon.ico" />
<script src="/iconfont.js" async></script>
</Head>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
@@ -37,6 +45,7 @@ export default function App({ Component, pageProps }: AppProps) {
</Layout>
</ChakraProvider>
</QueryClientProvider>
<Script src="/iconfont.js"></Script>
</>
);
}

View File

@@ -2,13 +2,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { connectToDatabase, User } from '@/service/mongo';
import { sendCode } from '@/service/utils/sendEmail';
import { EmailTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { email, type } = req.query;
const { email, type } = req.query as { email: string; type: `${EmailTypeEnum}` };
if (!email || !type) {
throw new Error('缺少参数');
@@ -16,6 +16,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 注册人数限流
if (type === EmailTypeEnum.register) {
const maxCount = process.env.MAX_USER ? +process.env.MAX_USER : Infinity;
const userCount = await User.count();
if (userCount >= maxCount) {
throw new Error('当前注册用户已满,请等待名额~');
}
}
let code = '';
for (let i = 0; i < 6; i++) {
code += Math.floor(Math.random() * 10);

View File

@@ -18,10 +18,12 @@ import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { OpenAiModelEnum } from '@/constants/model';
const textareaMinH = '22px';
const Chat = () => {
const { toast } = useToast();
const router = useRouter();
const { media } = useScreen();
const { isPc, media } = useScreen();
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
@@ -32,7 +34,7 @@ const Chat = () => {
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
const { Loading } = useLoading();
const { Loading, setIsLoading } = useLoading({ defaultLoading: true });
// 滚动到底部
const scrollToBottom = useCallback(() => {
@@ -47,28 +49,36 @@ const Chat = () => {
}, []);
// 初始化聊天框
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
cacheTime: 5 * 60 * 1000,
onSuccess(res) {
if (!res) return;
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
const { isInitialLoading } = useQuery(
[chatId, windowId],
() => (chatId ? getInitChatSiteInfo(chatId, windowId) : null),
{
cacheTime: 5 * 60 * 1000,
onSuccess(res) {
if (!res) return;
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
},
onError() {
toast({
title: '初始化异常',
status: 'error'
});
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
setIsLoading(false);
},
onError() {
toast({
title: '初始化异常,请刷新',
status: 'error',
isClosable: true,
duration: 5000
});
setIsLoading(false);
}
}
});
);
// gpt3 方法
const gpt3ChatPrompt = useCallback(
@@ -179,8 +189,9 @@ const Chat = () => {
setTimeout(() => {
scrollToBottom();
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height = 22 + 'px';
TextareaDom.current.style.height = textareaMinH;
}
}, 100);
@@ -242,7 +253,7 @@ const Chat = () => {
}, [chatList, windowId]);
return (
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
<Flex height={'100%'} flexDirection={'column'}>
{/* 头部 */}
<Flex
px={4}
@@ -258,7 +269,6 @@ const Chat = () => {
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
</Box>
{/* 滚动到底部按键 */}
{/* 滚动到底部 */}
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
<Icon
@@ -302,8 +312,9 @@ const Chat = () => {
<Box
m={media('20px auto', '0 auto')}
w={media('100vw', '100%')}
maxW={'800px'}
maxW={media('800px', 'auto')}
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
>
{lastWordHuman ? (
<Box textAlign={'center'}>
@@ -349,12 +360,12 @@ const Chat = () => {
onChange={(e) => {
const textarea = e.target;
setInputVal(textarea.value);
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
}}
onKeyDown={(e) => {
// 触发快捷发送
if (e.keyCode === 13 && !e.shiftKey) {
if (isPc && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
@@ -382,7 +393,7 @@ const Chat = () => {
</Box>
)}
</Box>
<Loading loading={!chatSiteData} />
<Loading />
</Flex>
);
};

View File

@@ -1,19 +1,12 @@
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '../../../constants/user';
import { postFindPassword } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
@@ -28,7 +21,7 @@ interface RegisterType {
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
@@ -57,6 +50,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, code, password }: RegisterType) => {
setRequesting(true);
try {
toast({
title: `密码已找回`,
status: 'success'
});
loginSuccess(
await postFindPassword({
email,
@@ -64,11 +61,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password
})
);
toast({
title: `密码已找回`,
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({

View File

@@ -32,16 +32,16 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password }: LoginFormType) => {
setRequesting(true);
try {
toast({
title: '登录成功',
status: 'success'
});
loginSuccess(
await postLogin({
email,
password
})
);
toast({
title: '登录成功',
status: 'success'
});
} catch (error) {
typeof error === 'string' &&
toast({

View File

@@ -1,19 +1,12 @@
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '@/constants/user';
import { postRegister } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
loginSuccess: (e: ResLogin) => void;
@@ -28,7 +21,7 @@ interface RegisterType {
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
@@ -57,6 +50,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password, code }: RegisterType) => {
setRequesting(true);
try {
toast({
title: `注册成功`,
status: 'success'
});
loginSuccess(
await postRegister({
email,
@@ -64,17 +61,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password
})
);
toast({
title: `注册成功`,
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
duration: 4000,
isClosable: true
});
}
setRequesting(false);

View File

@@ -1,7 +1,5 @@
.loginPage {
background: url('/icon/login-bg.svg') no-repeat;
background-size: cover;
height: 100vh;
width: 100vw;
user-select: none;
}

View File

@@ -40,7 +40,7 @@ const Login = () => {
};
return (
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
<Box className={styles.loginPage} h={'100%'} p={isPc ? '10vh 10vw' : 0}>
<Flex
maxW={'1240px'}
m={'auto'}

View File

@@ -8,29 +8,23 @@ import { useRouter } from 'next/router';
import ModelTable from './components/ModelTable';
import ModelPhoneList from './components/ModelPhoneList';
import { useScreen } from '@/hooks/useScreen';
import { useGlobalStore } from '@/store/global';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
const ModelList = () => {
const { isPc } = useScreen();
const router = useRouter();
const [models, setModels] = useState<ModelType[]>([]);
const [openCreateModel, setOpenCreateModel] = useState(false);
const { setLoading } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
/* 加载模型 */
const loadModels = useCallback(async () => {
setLoading(true);
try {
const res = await getMyModels();
const { isLoading } = useQuery(['loadModels'], () => getMyModels(), {
onSuccess(res) {
if (!res) return;
setModels(res);
} catch (err) {
console.log(err);
}
setLoading(false);
}, [setLoading]);
useEffect(() => {
loadModels();
}, [loadModels]);
});
/* 创建成功回调 */
const createModelSuccess = useCallback((data: ModelType) => {
@@ -40,7 +34,7 @@ const ModelList = () => {
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(
async (modelId: string) => {
setLoading(true);
setIsLoading(true);
try {
const chatId = await getChatSiteId(modelId);
@@ -50,9 +44,9 @@ const ModelList = () => {
} catch (err) {
console.log(err);
}
setLoading(false);
setIsLoading(false);
},
[router, setLoading]
[router, setIsLoading]
);
return (
@@ -83,6 +77,7 @@ const ModelList = () => {
setCreateModelOpen={setOpenCreateModel}
onSuccess={createModelSuccess}
/>
<Loading loading={isLoading} />
</Box>
);
};