feat: chat ui
This commit is contained in:
parent
39f9080eb2
commit
1226c3efb7
@ -11,5 +11,6 @@ chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部
|
|||||||
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
|
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
|
||||||
**其他问题**
|
**其他问题**
|
||||||
请 WX 联系: fastgpt123
|
请 WX 联系: fastgpt123
|
||||||

|
| 交流群 | 小助手 |
|
||||||

|
| ----------------------- | -------------------- |
|
||||||
|
|  |  |
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import Navbar from './navbar';
|
|||||||
import NavbarPhone from './navbarPhone';
|
import NavbarPhone from './navbarPhone';
|
||||||
|
|
||||||
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||||
|
'/': true,
|
||||||
'/login': true
|
'/login': true
|
||||||
};
|
};
|
||||||
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
||||||
|
'/': true,
|
||||||
'/login': true
|
'/login': true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -337,10 +337,10 @@
|
|||||||
}
|
}
|
||||||
.markdown {
|
.markdown {
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
overflow-y: hidden;
|
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
@ -353,13 +353,13 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: #222 !important;
|
background-color: #292b33 !important;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
background-color: #222 !important;
|
background-color: #292b33 !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
|||||||
const code = String(children);
|
const code = String(children);
|
||||||
|
|
||||||
return !inline || match ? (
|
return !inline || match ? (
|
||||||
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
|
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
|
||||||
<Flex
|
<Flex
|
||||||
className="code-header"
|
className="code-header"
|
||||||
py={2}
|
py={2}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {
|
import {
|
||||||
getInitChatSiteInfo,
|
getInitChatSiteInfo,
|
||||||
@ -26,7 +26,10 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
DrawerContent
|
DrawerContent,
|
||||||
|
Card,
|
||||||
|
Tooltip,
|
||||||
|
useOutsideClick
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
@ -76,6 +79,8 @@ const Chat = ({
|
|||||||
|
|
||||||
const ChatBox = useRef<HTMLDivElement>(null);
|
const ChatBox = useRef<HTMLDivElement>(null);
|
||||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const ContextMenuRef = useRef(null);
|
||||||
|
const PhoneContextShow = useRef(false);
|
||||||
|
|
||||||
// 中断请求
|
// 中断请求
|
||||||
const controller = useRef(new AbortController());
|
const controller = useRef(new AbortController());
|
||||||
@ -83,6 +88,12 @@ const Chat = ({
|
|||||||
|
|
||||||
const [inputVal, setInputVal] = useState(''); // user input prompt
|
const [inputVal, setInputVal] = useState(''); // user input prompt
|
||||||
const [showSystemPrompt, setShowSystemPrompt] = useState('');
|
const [showSystemPrompt, setShowSystemPrompt] = useState('');
|
||||||
|
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
||||||
|
// message messageContextMenuData
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
message: ChatSiteItemType;
|
||||||
|
}>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
lastChatModelId,
|
lastChatModelId,
|
||||||
@ -107,6 +118,24 @@ const Chat = ({
|
|||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
const { userInfo } = useUserStore();
|
const { userInfo } = useUserStore();
|
||||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||||
|
// close contextMenu
|
||||||
|
useOutsideClick({
|
||||||
|
ref: ContextMenuRef,
|
||||||
|
handler: () => {
|
||||||
|
// 移动端长按后会将其设置为true,松手时候也会触发一次,松手的时候需要忽略一次。
|
||||||
|
if (PhoneContextShow.current) {
|
||||||
|
PhoneContextShow.current = false;
|
||||||
|
} else {
|
||||||
|
messageContextMenuData &&
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessageContextMenuData(undefined);
|
||||||
|
window.getSelection?.()?.empty?.();
|
||||||
|
window.getSelection?.()?.removeAllRanges?.();
|
||||||
|
document?.getSelection()?.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
|
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||||
@ -234,6 +263,9 @@ const Chat = ({
|
|||||||
|
|
||||||
// refresh history
|
// refresh history
|
||||||
loadHistory({ pageNum: 1, init: true });
|
loadHistory({ pageNum: 1, init: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
generatingMessage();
|
||||||
|
}, 100);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
chatId,
|
chatId,
|
||||||
@ -327,24 +359,26 @@ const Chat = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 删除一句话
|
// 删除一句话
|
||||||
const delChatRecord = useCallback(
|
const delChatRecord = useCallback(async () => {
|
||||||
async (index: number, id: string) => {
|
if (!messageContextMenuData) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
const index = chatData.history.findIndex(
|
||||||
// 删除数据库最后一句
|
(item) => item._id === messageContextMenuData.message._id
|
||||||
await delChatRecordByIndex(chatId, id);
|
);
|
||||||
|
|
||||||
setChatData((state) => ({
|
try {
|
||||||
...state,
|
// 删除数据库最后一句
|
||||||
history: state.history.filter((_, i) => i !== index)
|
await delChatRecordByIndex(chatId, messageContextMenuData.message._id);
|
||||||
}));
|
|
||||||
} catch (err) {
|
setChatData((state) => ({
|
||||||
console.log(err);
|
...state,
|
||||||
}
|
history: state.history.filter((_, i) => i !== index)
|
||||||
setIsLoading(false);
|
}));
|
||||||
},
|
} catch (err) {
|
||||||
[chatId, setChatData, setIsLoading]
|
console.log(err);
|
||||||
);
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [chatData.history, chatId, messageContextMenuData, setChatData, setIsLoading]);
|
||||||
|
|
||||||
// 复制内容
|
// 复制内容
|
||||||
const onclickCopy = useCallback(
|
const onclickCopy = useCallback(
|
||||||
@ -433,6 +467,34 @@ const Chat = ({
|
|||||||
[loadHistory]
|
[loadHistory]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// onclick chat message context
|
||||||
|
const onclickContextMenu = useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>, message: ChatSiteItemType) => {
|
||||||
|
e.preventDefault(); // 阻止默认右键菜单
|
||||||
|
|
||||||
|
// select all text
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(e.currentTarget as HTMLDivElement);
|
||||||
|
window.getSelection()?.removeAllRanges();
|
||||||
|
window.getSelection()?.addRange(range);
|
||||||
|
|
||||||
|
navigator.vibrate?.(50); // 震动 50 毫秒
|
||||||
|
|
||||||
|
if (!isPcDevice) {
|
||||||
|
PhoneContextShow.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageContextMenuData({
|
||||||
|
left: e.clientX,
|
||||||
|
top: e.clientY,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[isPcDevice]
|
||||||
|
);
|
||||||
|
|
||||||
// 获取对话信息
|
// 获取对话信息
|
||||||
const loadChatInfo = useCallback(
|
const loadChatInfo = useCallback(
|
||||||
async ({
|
async ({
|
||||||
@ -511,6 +573,7 @@ const Chat = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// abort stream
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
isLeavePage.current = true;
|
isLeavePage.current = true;
|
||||||
@ -522,7 +585,7 @@ const Chat = ({
|
|||||||
<Flex
|
<Flex
|
||||||
h={'100%'}
|
h={'100%'}
|
||||||
flexDirection={['column', 'row']}
|
flexDirection={['column', 'row']}
|
||||||
backgroundColor={useColorModeValue('white', '')}
|
backgroundColor={useColorModeValue('#fefefe', '')}
|
||||||
>
|
>
|
||||||
{/* pc always show history. phone is only show when modelId is present */}
|
{/* pc always show history. phone is only show when modelId is present */}
|
||||||
{isPc || !modelId ? (
|
{isPc || !modelId ? (
|
||||||
@ -605,6 +668,7 @@ const Chat = ({
|
|||||||
position={'relative'}
|
position={'relative'}
|
||||||
h={[0, '100%']}
|
h={[0, '100%']}
|
||||||
w={['100%', 0]}
|
w={['100%', 0]}
|
||||||
|
pt={[2, 4]}
|
||||||
flex={'1 0 0'}
|
flex={'1 0 0'}
|
||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
>
|
>
|
||||||
@ -617,20 +681,25 @@ const Chat = ({
|
|||||||
w={'100%'}
|
w={'100%'}
|
||||||
overflow={'overlay'}
|
overflow={'overlay'}
|
||||||
>
|
>
|
||||||
{chatData.history.map((item, index) => (
|
<Box maxW={['auto', '800px', '1000px']} m={'auto'}>
|
||||||
<Box
|
{chatData.history.map((item, index) => (
|
||||||
key={item._id}
|
<Flex key={item._id} alignItems={'flex-start'} py={2} px={[2, 4]}>
|
||||||
py={[6, 9]}
|
{item.obj === 'Human' && <Box flex={1} />}
|
||||||
px={[2, 4]}
|
{/* avatar */}
|
||||||
backgroundColor={
|
<Box
|
||||||
index % 2 !== 0 ? useColorModeValue('blackAlpha.50', 'gray.700') : ''
|
{...(item.obj === 'AI'
|
||||||
}
|
? {
|
||||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
order: 1,
|
||||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
mr: ['6px', 4],
|
||||||
>
|
cursor: 'pointer',
|
||||||
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
|
onClick: () => router.push(`/model?modelId=${chatData.modelId}`)
|
||||||
<Menu autoSelect={false}>
|
}
|
||||||
<MenuButton as={Box} mr={[1, 4]} cursor={'pointer'}>
|
: {
|
||||||
|
order: 3,
|
||||||
|
ml: ['6px', 4]
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Tooltip label={item.obj === 'AI' ? 'AI助手详情' : ''}>
|
||||||
<Image
|
<Image
|
||||||
className="avatar"
|
className="avatar"
|
||||||
src={
|
src={
|
||||||
@ -639,75 +708,73 @@ const Chat = ({
|
|||||||
: chatData.model.avatar || LOGO_ICON
|
: chatData.model.avatar || LOGO_ICON
|
||||||
}
|
}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
w={['20px', '30px']}
|
w={['20px', '34px']}
|
||||||
maxH={'50px'}
|
h={['20px', '34px']}
|
||||||
|
borderRadius={'50%'}
|
||||||
objectFit={'contain'}
|
objectFit={'contain'}
|
||||||
/>
|
/>
|
||||||
</MenuButton>
|
</Tooltip>
|
||||||
<MenuList fontSize={'sm'}>
|
</Box>
|
||||||
{chatData.model.canUse && item.obj === 'AI' && (
|
{/* message */}
|
||||||
<MenuItem onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}>
|
<Flex order={2} maxW={['calc(100% - 50px)', '80%']}>
|
||||||
AI助手详情
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={() => onclickCopy(item.value)}>复制</MenuItem>
|
|
||||||
<MenuItem onClick={() => delChatRecord(index, item._id)}>删除该行</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
|
|
||||||
{item.obj === 'AI' ? (
|
{item.obj === 'AI' ? (
|
||||||
<>
|
<Box w={'100%'}>
|
||||||
<Markdown
|
{isPc && (
|
||||||
source={item.value}
|
<Box color={'myGray.600'} fontSize={'sm'} mb={1}>
|
||||||
isChatting={isChatting && index === chatData.history.length - 1}
|
{chatData.model.name}
|
||||||
/>
|
</Box>
|
||||||
{item.systemPrompt && (
|
|
||||||
<Button
|
|
||||||
size={'xs'}
|
|
||||||
mt={2}
|
|
||||||
fontWeight={'normal'}
|
|
||||||
colorScheme={'gray'}
|
|
||||||
variant={'outline'}
|
|
||||||
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
|
|
||||||
>
|
|
||||||
查看提示词
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</>
|
<Card
|
||||||
|
bg={'white'}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius={'0 8px 8px 8px'}
|
||||||
|
onContextMenu={(e) => onclickContextMenu(e, item)}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
source={item.value}
|
||||||
|
isChatting={isChatting && index === chatData.history.length - 1}
|
||||||
|
/>
|
||||||
|
{item.systemPrompt && (
|
||||||
|
<Button
|
||||||
|
size={'xs'}
|
||||||
|
mt={2}
|
||||||
|
fontWeight={'normal'}
|
||||||
|
colorScheme={'gray'}
|
||||||
|
variant={'outline'}
|
||||||
|
w={'90px'}
|
||||||
|
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
|
||||||
|
>
|
||||||
|
查看提示词
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box className="markdown" whiteSpace={'pre-wrap'}>
|
<Box>
|
||||||
<Box as={'p'}>{item.value}</Box>
|
{isPc && (
|
||||||
|
<Box color={'myGray.600'} mb={1} fontSize={'sm'} textAlign={'right'}>
|
||||||
|
Human
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Card
|
||||||
|
className="markdown"
|
||||||
|
whiteSpace={'pre-wrap'}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius={'8px 0 8px 8px'}
|
||||||
|
bg={'myBlue.300'}
|
||||||
|
onContextMenu={(e) => onclickContextMenu(e, item)}
|
||||||
|
>
|
||||||
|
<Box as={'p'}>{item.value}</Box>
|
||||||
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Flex>
|
||||||
{/* copy and clear icon */}
|
|
||||||
{isPc && (
|
|
||||||
<Flex h={'100%'} flexDirection={'column'} ml={2} w={'14px'} height={'100%'}>
|
|
||||||
<Box minH={'40px'} flex={1}>
|
|
||||||
<MyIcon
|
|
||||||
name="copy"
|
|
||||||
w={'14px'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
color={'blackAlpha.700'}
|
|
||||||
onClick={() => onclickCopy(item.value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<MyIcon
|
|
||||||
name="delete"
|
|
||||||
w={'14px'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
color={'blackAlpha.700'}
|
|
||||||
_hover={{
|
|
||||||
color: 'red.600'
|
|
||||||
}}
|
|
||||||
onClick={() => delChatRecord(index, item._id)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
))}
|
||||||
))}
|
{chatData.history.length === 0 && <Empty model={chatData.model} />}
|
||||||
{chatData.history.length === 0 && <Empty model={chatData.model} />}
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{/* 发送区 */}
|
{/* 发送区 */}
|
||||||
{chatData.model.canUse ? (
|
{chatData.model.canUse ? (
|
||||||
@ -715,7 +782,7 @@ const Chat = ({
|
|||||||
<Box
|
<Box
|
||||||
py={'18px'}
|
py={'18px'}
|
||||||
position={'relative'}
|
position={'relative'}
|
||||||
boxShadow={`0 0 15px rgba(0,0,0,0.1)`}
|
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
||||||
borderTop={['1px solid', 0]}
|
borderTop={['1px solid', 0]}
|
||||||
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
|
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
borderRadius={['none', 'md']}
|
borderRadius={['none', 'md']}
|
||||||
@ -751,7 +818,7 @@ const Chat = ({
|
|||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// 触发快捷发送
|
// 触发快捷发送
|
||||||
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
if (isPcDevice && e.keyCode === 13 && !e.shiftKey) {
|
||||||
sendPrompt();
|
sendPrompt();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@ -817,6 +884,25 @@ const Chat = ({
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
{/* context menu */}
|
||||||
|
{messageContextMenuData && (
|
||||||
|
<Box
|
||||||
|
zIndex={10}
|
||||||
|
position={'fixed'}
|
||||||
|
top={messageContextMenuData.top}
|
||||||
|
left={messageContextMenuData.left}
|
||||||
|
>
|
||||||
|
<Box ref={ContextMenuRef}></Box>
|
||||||
|
<Menu isOpen>
|
||||||
|
<MenuList minW={`100px !important`}>
|
||||||
|
<MenuItem onClick={() => onclickCopy(messageContextMenuData.message.value)}>
|
||||||
|
复制
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={delChatRecord}>删除</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -141,6 +141,7 @@ const Home = () => {
|
|||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
pt={'20vh'}
|
pt={'20vh'}
|
||||||
|
overflow={'overlay'}
|
||||||
>
|
>
|
||||||
<Box id={'particles-js'} position={'absolute'} top={0} left={0} right={0} bottom={0} />
|
<Box id={'particles-js'} position={'absolute'} top={0} left={0} right={0} bottom={0} />
|
||||||
<Image src="/icon/logo.png" w={['70px', '120px']} h={['70px', '120px']} alt={''}></Image>
|
<Image src="/icon/logo.png" w={['70px', '120px']} h={['70px', '120px']} alt={''}></Image>
|
||||||
@ -162,7 +163,8 @@ const Home = () => {
|
|||||||
<Button
|
<Button
|
||||||
my={5}
|
my={5}
|
||||||
fontSize={['xl', '3xl']}
|
fontSize={['xl', '3xl']}
|
||||||
h={['35px', '40px']}
|
h={'auto'}
|
||||||
|
py={[2, 3]}
|
||||||
onClick={() => router.push(`/model`)}
|
onClick={() => router.push(`/model`)}
|
||||||
>
|
>
|
||||||
点击开始
|
点击开始
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user