Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
sdream-ai-fe
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
侯明涛
sdream-ai-fe
Commits
d586835c
Commit
d586835c
authored
Dec 12, 2025
by
Liu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat:新增页面内会话部分逻辑
parent
9bf64ec0
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
157 additions
and
19 deletions
+157
-19
src/layouts/Navbar/Navbar.tsx
+18
-3
src/pages/ChatTactics/TacticsChat.tsx
+89
-14
src/pages/ChatTactics/TacticsHome.tsx
+50
-2
No files found.
src/layouts/Navbar/Navbar.tsx
View file @
d586835c
import
type
React
from
'react'
import
{
motion
}
from
'framer-motion'
import
{
useEffect
,
useRef
,
useState
}
from
'react'
import
{
useNavigate
}
from
'react-router-dom'
import
{
use
Location
,
use
Navigate
}
from
'react-router-dom'
import
{
useClickAway
,
useSessionStorageState
}
from
'ahooks'
import
styles
from
'./Navbar.module.less'
import
{
NavBarItem
}
from
'./components/NavBarItem'
import
{
clearNavigationFlag
,
createConversation
}
from
'@/store/conversationSlice'
import
{
createTacticsConversation
}
from
'@/store/tacticsSlice'
import
type
{
WithAuthProps
}
from
'@/auth/withAuth'
import
{
withAuth
}
from
'@/auth/withAuth'
import
{
NAV_BAR_ITEMS
}
from
'@/config/nav'
...
...
@@ -21,8 +22,10 @@ interface NavbarProps {
const
NavbarBase
:
React
.
FC
<
NavbarProps
&
WithAuthProps
>
=
({
isHistoryVisible
,
checkAuth
,
onSetHistoryVisible
})
=>
{
const
dispatch
=
useAppDispatch
()
const
navigate
=
useNavigate
()
const
location
=
useLocation
()
const
{
currentConversationId
,
shouldNavigateToNewConversation
,
currentToolId
}
=
useAppSelector
(
state
=>
state
.
conversation
)
const
{
currentConversationId
:
_tacticsConversationId
,
shouldNavigateToNewConversation
:
_tacticsShouldNavigate
}
=
useAppSelector
(
state
=>
state
.
tactics
)
const
handleCreateConversation
=
()
=>
{
const
sessionToolId
=
sessionStorage
.
getItem
(
'currentToolId'
)
||
undefined
...
...
@@ -35,6 +38,14 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
}))
}
const
handleCreateTacticsConversation
=
()
=>
{
dispatch
(
createTacticsConversation
({
conversationData
:
{},
shouldNavigate
:
true
,
shouldSendQuestion
:
''
,
}))
}
const
[
isH5NavVisible
,
setIsH5NavVisible
]
=
useState
(
isMobile
())
const
handleClick
=
(
type
:
string
|
undefined
)
=>
{
...
...
@@ -54,7 +65,11 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
}
if
(
type
===
'add'
)
{
if
(
location
.
pathname
.
includes
(
'/chat'
))
{
// 判断是否为 tactics 聊天页面
if
(
location
.
pathname
.
startsWith
(
'/tactics/chat'
))
{
handleCreateTacticsConversation
()
}
else
if
(
location
.
pathname
.
includes
(
'/chat'
))
{
handleCreateConversation
()
}
else
{
...
...
@@ -110,7 +125,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
const
url
=
currentToolId
?
`/chat/
${
currentConversationId
}
?toolId=
${
currentToolId
}
`
:
`/chat/
${
currentConversationId
}
`
// 通过 location.state 传递 toolId,避免在 Chat 页面被误判为
“外链残留参数”
而强制清空
// 通过 location.state 传递 toolId,避免在 Chat 页面被误判为
"外链残留参数"
而强制清空
navigate
(
url
,
{
state
:
{
toolId
:
currentToolId
||
null
,
...
...
src/pages/ChatTactics/TacticsChat.tsx
View file @
d586835c
// 问答功能独立聊天页
import
React
,
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
'react'
import
{
useLocation
,
useNavigate
,
useParams
}
from
'react-router-dom'
import
{
useLocalStorageState
,
useScroll
}
from
'ahooks'
import
{
Button
}
from
'@heroui/react'
import
{
motion
}
from
'framer-motion'
import
{
useScroll
}
from
'ahooks'
import
styles
from
'../Chat/Chat.module.less'
import
{
processApiResponse
}
from
'../Chat/helper'
import
{
ChatItemUser
}
from
'../Chat/components/ChatItem/ChatItemUser'
...
...
@@ -13,7 +13,8 @@ import { ChatEditor } from '@/components/ChatEditor'
import
type
{
ChatRecord
}
from
'@/types/chat'
import
{
fetchTacticsQaRecordPage
}
from
'@/api/tactics'
import
{
fetchCheckTokenApi
,
fetchStreamResponse
}
from
'@/api/chat'
import
{
clearTacticsNavigationFlag
,
clearTacticsShouldSendQuestion
,
createTacticsConversation
}
from
'@/store/tacticsSlice'
import
{
fetchLoginByToken
,
fetchLoginByUid
}
from
'@/api/common'
import
{
clearTacticsNavigationFlag
,
clearTacticsShouldSendQuestion
,
createTacticsConversation
,
fetchTacticsConversations
}
from
'@/store/tacticsSlice'
import
type
{
RootState
}
from
'@/store'
import
{
useAppDispatch
,
useAppSelector
}
from
'@/store/hook'
import
ScrollBtoIcon
from
'@/assets/svg/scrollBto.svg?react'
...
...
@@ -24,6 +25,7 @@ export const TacticsChat: React.FC = () => {
const
{
id
}
=
useParams
<
{
id
:
string
}
>
()
const
location
=
useLocation
()
const
navigate
=
useNavigate
()
const
viteOutputObj
=
import
.
meta
.
env
.
VITE_OUTPUT_OBJ
||
'open'
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
)
const
[
allItems
,
setAllItems
]
=
useState
<
ChatRecord
[]
>
([])
const
dispatch
=
useAppDispatch
()
...
...
@@ -32,6 +34,12 @@ export const TacticsChat: React.FC = () => {
shouldNavigateToNewConversation
,
currentConversationId
,
}
=
useAppSelector
((
state
:
RootState
)
=>
state
.
tactics
)
const
hasFetched
=
useRef
(
false
)
const
hasCreatedRef
=
useRef
(
false
)
// 使用 useLocalStorageState 管理 token,与原有逻辑保持一致
const
[
token
,
setToken
]
=
useLocalStorageState
<
string
|
undefined
>
(
'__TOKEN__'
,
{
defaultValue
:
''
,
})
// 优先从 location.state 获取,其次从 Redux state 获取
const
shouldSendQuestion
=
(
location
.
state
as
{
shouldSendQuestion
?:
string
}
|
null
)?.
shouldSendQuestion
||
shouldSendQuestionFromState
const
scrollableRef
=
useRef
<
HTMLDivElement
|
any
>
(
null
)
...
...
@@ -39,7 +47,79 @@ export const TacticsChat: React.FC = () => {
const
currentIdRef
=
useRef
<
string
|
undefined
>
(
id
)
const
lastSentQuestionRef
=
useRef
<
string
>
(
''
)
const
abortControllerRef
=
useRef
<
AbortController
|
null
>
(
null
)
const
hasCreatedRef
=
useRef
(
false
)
// 创建新会话(仅在 tactics 聊天页面且没有 id 时调用)
const
initTacticsConversation
=
useCallback
(()
=>
{
// 只有在 tactics 聊天页面且没有会话 id 时才创建新对话
if
(
!
id
&&
location
.
pathname
.
startsWith
(
'/tactics/chat'
))
{
if
(
hasCreatedRef
.
current
)
{
return
}
hasCreatedRef
.
current
=
true
dispatch
(
createTacticsConversation
({
conversationData
:
{},
shouldNavigate
:
true
,
shouldSendQuestion
:
''
,
}),
)
}
},
[
id
,
location
.
pathname
,
dispatch
])
// 登录逻辑(复用原有逻辑,与 TacticsHome.tsx 保持一致)
const
login
=
useCallback
(
async
()
=>
{
if
(
hasFetched
.
current
)
{
return
}
hasFetched
.
current
=
true
const
url
=
new
URL
(
window
.
location
.
href
)
const
searchParams
=
new
URLSearchParams
(
url
.
search
)
const
_loginCode
=
searchParams
.
get
(
'loginCode'
)
let
res
=
{}
as
any
if
(
viteOutputObj
===
'inner'
)
{
if
(
_loginCode
)
{
res
=
await
fetchLoginByToken
(
_loginCode
)
if
(
res
.
data
)
{
setToken
(
res
.
data
.
token
)
window
.
dispatchEvent
(
new
StorageEvent
(
'storage'
,
{
key
:
'__TOKEN__'
,
oldValue
:
token
,
newValue
:
res
.
data
.
token
,
url
:
window
.
location
.
href
,
storageArea
:
localStorage
,
}),
)
// 登录成功后,如果是 tactics 聊天页面且没有 id,则创建会话
initTacticsConversation
()
dispatch
(
fetchTacticsConversations
())
}
}
else
{
// 如果没有 loginCode,但已有 token,直接尝试创建会话
initTacticsConversation
()
dispatch
(
fetchTacticsConversations
())
}
}
else
{
res
=
await
fetchLoginByUid
(
'123123'
)
if
(
res
.
data
)
{
setToken
(
res
.
data
.
token
)
window
.
dispatchEvent
(
new
StorageEvent
(
'storage'
,
{
key
:
'__TOKEN__'
,
oldValue
:
token
,
newValue
:
res
.
data
.
token
,
url
:
window
.
location
.
href
,
storageArea
:
localStorage
,
}),
)
// 登录成功后,如果是 tactics 聊天页面且没有 id,则创建会话
initTacticsConversation
()
dispatch
(
fetchTacticsConversations
())
}
}
},
[
setToken
,
dispatch
,
token
,
initTacticsConversation
,
viteOutputObj
])
/** 处理正常stream的数据 */
const
handleStreamMesageData
=
(
msg
:
any
,
question
:
string
)
=>
{
...
...
@@ -229,22 +309,17 @@ export const TacticsChat: React.FC = () => {
getUserQaRecordPage
(
id
)
}
else
{
// 如果没有 id,进入页面时创建新会话
if
(
!
hasCreatedRef
.
current
)
{
hasCreatedRef
.
current
=
true
dispatch
(
createTacticsConversation
({
conversationData
:
{},
shouldNavigate
:
true
,
shouldSendQuestion
:
''
,
}),
)
}
// 如果没有 id,显示欢迎语,等待登录成功后创建新会话
setAllItems
([{
role
:
'system'
}
as
ChatRecord
])
setIsLoading
(
false
)
}
},
[
id
,
getUserQaRecordPage
,
dispatch
])
// 初始化时调用登录(登录成功后会自动创建会话)
useEffect
(()
=>
{
login
()
},
[
login
])
// 创建新会话成功后跳转到新会话页面
useEffect
(()
=>
{
if
(
shouldNavigateToNewConversation
&&
currentConversationId
)
{
...
...
src/pages/ChatTactics/TacticsHome.tsx
View file @
d586835c
...
...
@@ -12,6 +12,7 @@ import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import
{
getUserRolesFromRoute
}
from
'@/lib/utils'
import
{
ChatEditor
}
from
'@/components/ChatEditor'
import
{
RECOMMEND_QUESTIONS_OTHER
}
from
'@/config/recommendQuestion'
import
{
fetchCheckTokenApi
,
fetchStreamResponse
}
from
'@/api/chat'
export
const
TacticsHome
:
React
.
FC
=
()
=>
{
const
viteOutputObj
=
import
.
meta
.
env
.
VITE_OUTPUT_OBJ
||
'open'
...
...
@@ -24,6 +25,7 @@ export const TacticsHome: React.FC = () => {
const
[
token
,
setToken
]
=
useLocalStorageState
<
string
|
undefined
>
(
'__TOKEN__'
,
{
defaultValue
:
''
,
})
const
abortControllerRef
=
useRef
<
AbortController
|
null
>
(
null
)
const
initTacticsConversation
=
()
=>
{
const
fromCollect
=
location
.
state
?.
fromCollect
...
...
@@ -44,7 +46,53 @@ export const TacticsHome: React.FC = () => {
}
// 处理创建对话并跳转(用于输入框提交)
const
handleCreateConversation
=
useCallback
((
question
:
string
)
=>
{
const
handleCreateConversation
=
useCallback
(
async
(
question
:
string
)
=>
{
// 如果已有会话,直接调用 submit 接口提交问题,然后跳转到聊天页面
if
(
currentConversationId
)
{
// 停止之前的请求
if
(
abortControllerRef
.
current
)
{
abortControllerRef
.
current
.
abort
()
}
// 检查token
await
fetchCheckTokenApi
()
// 创建新的 AbortController
abortControllerRef
.
current
=
new
AbortController
()
let
fetchUrl
=
`/conversation/api/conversation/mobile/v1/submit_question_stream`
const
viteOutputObj
=
import
.
meta
.
env
.
VITE_OUTPUT_OBJ
||
'open'
let
proxy
=
''
if
(
viteOutputObj
===
'open'
)
{
proxy
=
import
.
meta
.
env
.
MODE
!==
'prod'
?
'/api'
:
'/dev-sdream-api'
}
else
{
proxy
=
import
.
meta
.
env
.
MODE
===
'dev'
?
'/api'
:
'/dev-sdream-api'
}
fetchUrl
=
proxy
+
fetchUrl
// 直接调用 submit 接口(消息流会在聊天页面处理,这里只负责发起请求)
fetchStreamResponse
(
fetchUrl
,
{
question
,
conversationId
:
currentConversationId
,
stream
:
true
,
},
()
=>
{
// 在首页不需要处理消息,跳转到聊天页面后会自动刷新加载最新消息
},
abortControllerRef
.
current
.
signal
,
)
// 跳转到聊天页面查看结果
navigate
(
`/tactics/chat/
${
currentConversationId
}
`
)
return
}
// 如果没有会话,才创建新会话
dispatch
(
createTacticsConversation
({
conversationData
:
{},
...
...
@@ -52,7 +100,7 @@ export const TacticsHome: React.FC = () => {
shouldSendQuestion
:
question
,
}),
)
},
[
dispatch
])
},
[
dispatch
,
currentConversationId
,
navigate
])
// 监听导航到新对话
useEffect
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment