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
122b11f9
Commit
122b11f9
authored
Dec 01, 2025
by
Liu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix:历史纪录展示逻辑
parent
68a333d9
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
296 additions
and
45 deletions
+296
-45
src/api/home.ts
+14
-2
src/api/mock/conversation.ts
+106
-0
src/api/mock/home.ts
+30
-0
src/components/ChatEditor/index.tsx
+86
-21
src/env.d.ts
+5
-0
src/layouts/HistoryBar/components/HistoryBarList/index.tsx
+6
-2
src/layouts/MainLayout/MainLayout.tsx
+6
-7
src/pages/Home/components/QuestionList/QuestionList.tsx
+9
-11
src/store/conversationSlice.helper.ts
+15
-0
src/store/conversationSlice.ts
+19
-2
No files found.
src/api/home.ts
View file @
122b11f9
import
http
from
'@/utils/request'
import
{
mockFetchEfficiencyQuestionList
}
from
'@/api/mock/home'
/**
* 查询推荐问题列表
...
...
@@ -27,6 +28,17 @@ export function fetchToolList(params?: { userRoles?: string[] }) {
* @params
* toolId: 工具id
*/
export
function
fetchEfficiencyQuestionList
(
data
:
any
)
{
return
http
.
post
(
'/conversation/api/conversation/mobile/v1/generate_question'
,
data
)
const
shouldUseEfficiencyMock
=
(
import
.
meta
as
any
).
env
?.
VITE_USE_EFFICIENCY_MOCK
===
'true'
export
async
function
fetchEfficiencyQuestionList
(
data
:
any
)
{
if
(
shouldUseEfficiencyMock
)
return
mockFetchEfficiencyQuestionList
()
try
{
return
await
http
.
post
(
'/conversation/api/conversation/mobile/v1/generate_question'
,
data
)
}
catch
(
error
)
{
console
.
warn
(
'fetchEfficiencyQuestionList fallback to mock due to error:'
,
error
)
return
mockFetchEfficiencyQuestionList
()
}
}
src/api/mock/conversation.ts
0 → 100644
View file @
122b11f9
import
{
subDays
,
subMinutes
}
from
'date-fns'
import
type
{
Conversation
}
from
'@/types/conversation'
interface
ConversationPageResponse
{
code
:
string
message
:
string
data
:
{
records
:
Conversation
[]
total
:
number
size
:
number
current
:
number
pages
:
number
}
ok
:
boolean
}
const
now
=
new
Date
()
function
formatDate
(
date
:
Date
)
{
return
date
.
toISOString
().
slice
(
0
,
19
).
replace
(
'T'
,
' '
)
}
function
createMockConversation
({
id
,
title
,
daysAgo
=
0
,
minutesAgo
=
0
,
qaNum
,
toolId
,
isCurrent
=
false
,
}:
{
id
:
string
title
:
string
daysAgo
?:
number
minutesAgo
?:
number
qaNum
:
number
toolId
?:
string
isCurrent
?:
boolean
}):
Conversation
{
const
endTime
=
subMinutes
(
subDays
(
now
,
daysAgo
),
minutesAgo
)
const
startTime
=
subMinutes
(
endTime
,
5
)
return
{
conversationId
:
id
,
conversationTitle
:
title
,
currentConversationFlag
:
isCurrent
,
startTime
:
formatDate
(
startTime
),
endTime
:
formatDate
(
endTime
),
qaNum
,
...(
toolId
?
{
toolId
}
:
{}),
}
}
const
conversationRecords
:
Conversation
[]
=
[
createMockConversation
({
id
:
'1995419273074701146'
,
title
:
'未知会话内容'
,
qaNum
:
3
,
toolId
:
'6712395743240'
,
isCurrent
:
true
,
}),
createMockConversation
({
id
:
'1995417608679268353'
,
title
:
'资产审评的目标及关键要素1'
,
qaNum
:
1
,
toolId
:
'6712395743241'
,
minutesAgo
:
20
,
}),
createMockConversation
({
id
:
'19954143358382255489'
,
title
:
'对公授信材料清单2'
,
qaNum
:
4
,
daysAgo
:
2
,
minutesAgo
:
10
,
toolId
:
'6712395743241'
,
}),
createMockConversation
({
id
:
'19954114588762233881'
,
title
:
'项目贷款调研要点3'
,
toolId
:
'6712395743240'
,
qaNum
:
2
,
daysAgo
:
9
,
}),
createMockConversation
({
id
:
'19954099873351298711'
,
title
:
'投融资合规的最新政策4'
,
qaNum
:
6
,
daysAgo
:
21
,
}),
]
const
conversationPageMockResponse
:
ConversationPageResponse
=
{
code
:
'00000000'
,
message
:
'成功'
,
data
:
{
records
:
conversationRecords
,
total
:
conversationRecords
.
length
,
size
:
100
,
current
:
1
,
pages
:
1
,
},
ok
:
true
,
}
export
function
mockFetchConversationPage
()
{
return
Promise
.
resolve
(
conversationPageMockResponse
)
}
src/api/mock/home.ts
0 → 100644
View file @
122b11f9
interface
EfficiencyQuestionResponse
{
code
:
string
message
:
string
data
:
{
questions
:
string
[]
}
ok
:
boolean
}
const
efficiencyQuestionMockResponse
:
EfficiencyQuestionResponse
=
{
code
:
'00000000'
,
message
:
'成功'
,
data
:
{
questions
:
[
'灾备部署有哪些要求?'
,
'触达中心如何设计渠道?'
,
'用户旅程设计如何整合信息?'
,
'文本管理应该具备哪些能力?'
,
'差旅规则有哪些?'
,
'反洗钱小组的职责是什么?'
,
'有效投诉的处理流程是什么?'
,
'公司项目审核会主要从哪几个方面审核项目?'
,
],
},
ok
:
true
,
}
export
function
mockFetchEfficiencyQuestionList
():
Promise
<
EfficiencyQuestionResponse
>
{
return
Promise
.
resolve
(
efficiencyQuestionMockResponse
)
}
src/components/ChatEditor/index.tsx
View file @
122b11f9
...
...
@@ -11,6 +11,36 @@ import { fetchToolList } from '@/api/home'
import
{
clearCurrentToolId
,
createConversation
,
setCurrentToolId
}
from
'@/store/conversationSlice'
import
{
getUserRolesForApi
}
from
'@/lib/utils'
const
MOCK_TOOL_LIST
=
[
{
toolId
:
'6712395743241'
,
toolName
:
'提质增效'
,
toolContent
:
'https://sit-wechat.guominpension.com/underwrite'
,
toolIcon
:
'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-increase.svg'
,
toolType
:
'03'
,
userRole
:
'02'
,
showOrder
:
8
,
},
{
toolId
:
'6712395743240'
,
toolName
:
'数据助手'
,
toolContent
:
'https://sit-wechat.guominpension.com/underwrite'
,
toolIcon
:
'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-data.svg'
,
toolType
:
'03'
,
userRole
:
'01'
,
showOrder
:
8
,
},
{
toolId
:
'general-mode'
,
toolName
:
'通用模式'
,
toolContent
:
'https://sit-wechat.guominpension.com/underwrite'
,
toolIcon
:
'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-normal.svg'
,
toolType
:
'01'
,
userRole
:
'00'
,
showOrder
:
8
,
},
]
as
const
interface
ChatEditorProps
{
onChange
?:
(
value
:
string
)
=>
void
onFocus
?:
()
=>
void
...
...
@@ -35,6 +65,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const
[
isToolBtnActive
,
setIsToolBtnActive
]
=
useState
<
boolean
>
(
true
)
const
currentToolId
=
useAppSelector
((
state
:
RootState
)
=>
state
.
conversation
.
currentToolId
)
const
[
showToolQuestion
,
setShowToolQuestion
]
=
useState
<
boolean
>
(
false
)
const
[
sessionToolId
,
setSessionToolId
]
=
useState
<
string
|
null
>
(
null
)
// 获取工具列表
const
getToolList
=
async
()
=>
{
...
...
@@ -49,33 +80,57 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
)
setToolList
(
uniqueList
)
}
else
{
setToolList
([...
MOCK_TOOL_LIST
])
}
}
catch
(
error
)
{
console
.
error
(
'获取工具列表失败:'
,
error
)
setToolList
([...
MOCK_TOOL_LIST
])
}
}
//
保持当前工具状态与 Redux 中的同步,确保跨页面返回时仍保持原模式
//
根据 currentToolId 以及 sessionStorage 中的记录决定高亮逻辑
useEffect
(()
=>
{
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor] currentToolId 变化:'
,
{
console
.
log
(
'[ChatEditor] currentToolId
或 sessionToolId
变化:'
,
{
currentToolId
,
selectedToolId
,
isToolBtnActive
,
sessionToolId
,
})
if
(
currentToolId
)
{
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor]
设置 selectedToolId
:'
,
currentToolId
)
console
.
log
(
'[ChatEditor]
高亮工具按钮
:'
,
currentToolId
)
setSelectedToolId
(
currentToolId
)
setIsToolBtnActive
(
false
)
return
}
if
(
!
currentToolId
&&
sessionToolId
)
{
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor] 使用 sessionToolId 恢复工具高亮:'
,
sessionToolId
)
setSelectedToolId
(
sessionToolId
)
setIsToolBtnActive
(
false
)
}
else
{
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor]
清除 selectedToolId,激活
通用模式'
)
console
.
log
(
'[ChatEditor]
没有 currentToolId,回到
通用模式'
)
setSelectedToolId
(
null
)
setIsToolBtnActive
(
true
)
}
},
[
currentToolId
])
},
[
currentToolId
,
sessionToolId
])
// 监听 sessionStorage 中的 currentToolId(历史点击时写入)来辅助高亮逻辑
useEffect
(()
=>
{
const
syncSessionToolId
=
()
=>
{
const
storedToolId
=
sessionStorage
.
getItem
(
'currentToolId'
)
setSessionToolId
(
storedToolId
)
}
syncSessionToolId
()
window
.
addEventListener
(
'storage'
,
syncSessionToolId
)
return
()
=>
{
window
.
removeEventListener
(
'storage'
,
syncSessionToolId
)
}
},
[])
const
startAnimation
=
()
=>
{
intervalRef
.
current
=
setInterval
(()
=>
{
...
...
@@ -147,12 +202,16 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const
handleGeneralClick
=
async
()
=>
{
if
(
!
checkAuth
())
return
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor] 点击通用模式按钮'
)
// 先更新 Redux,确保状态同步
dispatch
(
clearCurrentToolId
())
// 立即更新本地状态,让 UI 立即响应
setIsToolBtnActive
(
true
)
setSelectedToolId
(
null
)
sessionStorage
.
removeItem
(
'showToolQuestion'
)
sessionStorage
.
removeItem
(
'currentToolId'
)
setSessionToolId
(
null
)
setShowToolQuestion
(
false
)
try
{
await
dispatch
(
createConversation
({
...
...
@@ -171,6 +230,11 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const
handleToolClick
=
async
(
tool
:
any
)
=>
{
if
(
!
checkAuth
())
return
// eslint-disable-next-line no-console
console
.
log
(
'[ChatEditor] 点击工具按钮:'
,
{
toolId
:
tool
.
toolId
,
toolName
:
tool
.
toolName
,
})
if
(
tool
.
toolName
===
'数据助手'
)
{
sessionStorage
.
setItem
(
'showToolQuestion'
,
'true'
)
setShowToolQuestion
(
true
)
...
...
@@ -182,6 +246,8 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
dispatch
(
setCurrentToolId
(
tool
.
toolId
))
setSelectedToolId
(
tool
.
toolId
)
setIsToolBtnActive
(
false
)
sessionStorage
.
setItem
(
'currentToolId'
,
tool
.
toolId
)
setSessionToolId
(
tool
.
toolId
)
try
{
await
dispatch
(
createConversation
({
conversationData
:
{
toolId
:
tool
.
toolId
},
...
...
@@ -327,17 +393,15 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
isToolBtnActive
,
})
}
const
buttonStyles
=
isSelected
?
{
backgroundColor
:
'#F3F7FF'
,
borderColor
:
'#F3F7FF'
,
color
:
'#165DFF'
,
}
:
{
backgroundColor
:
'#FFFFFF'
,
borderColor
:
'#E6E8EB'
,
color
:
'#111827'
,
}
const
baseBtnClass
=
'w-auto h-[32px] px-3 rounded-full shadow-none text-[12px] flex items-center gap-2 transition-all duration-200 border'
const
selectedClass
=
isSelected
?
' bg-[#F3F7FF] border-[#AECBFF] text-[#165DFF]'
:
' bg-[#FFFFFF] border-[#E6E8EB] text-[#111827]'
const
selectedVariant
=
isSelected
?
'solid'
:
'bordered'
const
selectedColor
=
isSelected
?
'primary'
:
'default'
const
handleButtonPress
=
async
()
=>
{
// 高亮状态直接返回,避免重复触发
if
(
isSelected
)
...
...
@@ -347,14 +411,15 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
else
await
handleToolClick
(
tool
)
}
return
(
<
Button
key=
{
tool
.
toolId
||
`tool-${index}`
}
className=
"w-auto h-[32px] px-3 rounded-full shadow-none text-[12px] flex items-center gap-2 transition-all duration-200 border"
className=
{
`${baseBtnClass}${selectedClass}`
}
radius=
"full"
variant=
"bordered"
variant=
{
selectedVariant
}
color=
{
selectedColor
}
onPress=
{
handleButtonPress
}
style=
{
buttonStyles
}
>
{
tool
.
toolIcon
&&
(
<
img
...
...
src/env.d.ts
View file @
122b11f9
...
...
@@ -7,3 +7,8 @@ declare module '*.svg?react' {
const
ReactComponent
:
React
.
FunctionComponent
<
React
.
SVGProps
<
SVGSVGElement
>>
export
default
ReactComponent
}
interface
ImportMetaEnv
{
readonly
VITE_USE_EFFICIENCY_MOCK
?:
string
readonly
VITE_USE_CONVERSATION_MOCK
?:
string
}
src/layouts/HistoryBar/components/HistoryBarList/index.tsx
View file @
122b11f9
...
...
@@ -27,16 +27,20 @@ export const HistoryBarList: React.FC<HistoryBarListProps> = ({ searchValue, onS
onSetHistoryVisible
(
false
)
}
// eslint-disable-next-line no-console
console
.
log
(
'
[HistoryBarList] 点击历史记录:'
,
{
console
.
log
(
'
88888888888:'
,
conversation
,
{
conversationId
:
conversation
.
conversationId
,
toolId
:
conversation
.
toolId
,
})
if
(
conversation
.
toolId
)
{
// 将当前会话的 toolId 写入 sessionStorage,供 ChatEditor 使用
sessionStorage
.
setItem
(
'currentToolId'
,
conversation
.
toolId
)
// eslint-disable-next-line no-console
console
.
log
(
'
[HistoryBarList] 设置 toolId 到 Redux
:'
,
conversation
.
toolId
)
console
.
log
(
'
889999999999
:'
,
conversation
.
toolId
)
dispatch
(
setCurrentToolId
(
conversation
.
toolId
))
}
else
{
// 没有 toolId 时,移除 session 中的记录
sessionStorage
.
removeItem
(
'currentToolId'
)
// eslint-disable-next-line no-console
console
.
log
(
'[HistoryBarList] 清除 toolId'
)
dispatch
(
clearCurrentToolId
())
...
...
src/layouts/MainLayout/MainLayout.tsx
View file @
122b11f9
...
...
@@ -10,6 +10,8 @@ import { useAuth } from '@/auth/AuthContext'
import
{
LoginModal
}
from
'@/components/LoginModal'
import
MingcuteArrowsRightFill
from
'@/assets/svg/MingcuteArrowsRightFill.svg?react'
import
{
isMobile
}
from
'@/utils'
import
{
useAppDispatch
}
from
'@/store/hook'
import
{
fetchConversations
}
from
'@/store/conversationSlice'
interface
MainLayoutProps
{
children
:
React
.
ReactNode
...
...
@@ -40,8 +42,7 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const
{
showLoginModal
,
toggleLoginModal
}
=
useAuth
()
const
[
isHistoryVisible
,
setHistoryVisible
]
=
useState
(
false
)
const
location
=
useLocation
()
// const dispatch = useAppDispatch()
// const token = window.localStorage.getItem('__TOKEN__')
const
dispatch
=
useAppDispatch
()
const
[
navBarVisibleLocal
]
=
useSessionStorageState
<
string
|
undefined
>
(
'__NAV_BAR_VISIBLE_LOCAL__'
,
{
...
...
@@ -50,11 +51,9 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
},
)
// useEffect(() => {
// if (token) {
// dispatch(fetchConversations())
// }
// }, [dispatch])
useEffect
(()
=>
{
dispatch
(
fetchConversations
())
},
[
dispatch
])
useEffect
(()
=>
{
if
(
location
.
pathname
===
'/tools'
||
location
.
pathname
===
'/collect'
)
{
...
...
src/pages/Home/components/QuestionList/QuestionList.tsx
View file @
122b11f9
import
{
Button
,
Tooltip
}
from
'@heroui/react'
import
{
Tooltip
}
from
'@heroui/react'
import
type
React
from
'react'
import
{
Image
}
from
'@heroui/image'
import
{
motion
}
from
'framer-motion'
...
...
@@ -122,6 +122,7 @@ const QuestionListBase: React.FC<QuestionListProps & WithAuthProps> = ({
useEffect
(()
=>
{
updateDisplayedItems
()
},
[
updateDisplayedItems
])
return
(
<
div
className=
"bg-white box-border px-[20px] py-[12px] w-full sm:w-[300px] md:w-[300px]"
...
...
@@ -182,19 +183,16 @@ const QuestionListBase: React.FC<QuestionListProps & WithAuthProps> = ({
layout
className=
"w-full"
>
<
Button
onPress=
{
()
=>
handleClick
(
item
)
}
color=
"primary"
variant=
"light"
className=
"text-left bg-[#F8FBFF] w-full text-[#333] rounded-[23px] data-[hover=true]:bg-[#F0F8FF] data-[hover=true]:text-primary h-8"
>
<
Tooltip
color=
"foreground"
content=
{
item
}
placement=
"top"
>
<
div
className=
"w-full text-nowrap text-ellipsis overflow-hidden"
>
<
button
type=
"button"
onClick=
{
()
=>
handleClick
(
item
)
}
className=
"group w-full h-8 px-[14px] rounded-[23px] bg-[#F8FBFF] text-[#333] text-left flex items-center transition-colors hover:bg-[#F0F8FF] hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#29B6FD]/40"
>
<
span
className=
"w-[6px] h-[6px] rounded-full inline-block mr-[6px]"
style=
{
{
backgroundColor
:
dotColor
}
}
></
span
>
<
span
className=
"ml-[8
px]"
>
{
item
}
</
span
>
</
div
>
<
span
className=
"ml-[8px] truncate group-hover:text-primary text-[14
px]"
>
{
item
}
</
span
>
</
button
>
</
Tooltip
>
</
Button
>
</
motion
.
li
>
))
}
</
motion
.
ul
>
...
...
src/store/conversationSlice.helper.ts
View file @
122b11f9
...
...
@@ -9,6 +9,7 @@ export function processConversationData(records: Conversation[]): Conversation[]
let
hasToday
=
false
let
hasThisWeek
=
false
let
hasLast30Days
=
false
let
hasOlder
=
false
records
.
forEach
((
item
)
=>
{
const
endDate
=
new
Date
(
item
.
endTime
)
...
...
@@ -58,6 +59,20 @@ export function processConversationData(records: Conversation[]): Conversation[]
...
item
,
})
}
else
{
if
(
!
hasOlder
)
{
processedData
.
push
({
conversationId
:
''
,
conversationTitle
:
'更早'
,
currentConversationFlag
:
false
,
endTime
:
''
,
}
as
Conversation
)
hasOlder
=
true
}
processedData
.
push
({
...
item
,
})
}
})
return
processedData
...
...
src/store/conversationSlice.ts
View file @
122b11f9
...
...
@@ -3,6 +3,9 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import
{
processConversationData
}
from
'./conversationSlice.helper'
import
type
{
Conversation
,
ConversationState
}
from
'@/types/conversation'
import
{
fetchCreateConversation
,
fetchDeleteUserConversation
,
fetchQueryUserConversationPage
}
from
'@/api/conversation'
import
{
mockFetchConversationPage
}
from
'@/api/mock/conversation'
const
shouldUseConversationMock
=
(
import
.
meta
as
any
).
env
?.
VITE_USE_CONVERSATION_MOCK
===
'true'
const
initialState
:
ConversationState
=
{
conversations
:
[],
...
...
@@ -18,16 +21,30 @@ export const fetchConversations = createAsyncThunk(
'conversation/fetchConversations'
,
async
(
_
,
{
rejectWithValue
})
=>
{
try
{
const
response
=
await
fetchQueryUserConversationPage
({
const
response
=
shouldUseConversationMock
?
await
mockFetchConversationPage
()
:
await
fetchQueryUserConversationPage
({
keyword
:
''
,
pageNum
:
0
,
pageSize
:
100
,
})
const
processedData
=
processConversationData
(
response
.
data
.
records
)
const
records
=
response
.
data
.
records
const
processedData
=
records
.
length
===
0
&&
!
shouldUseConversationMock
?
processConversationData
((
await
mockFetchConversationPage
()).
data
.
records
)
:
processConversationData
(
records
)
return
processedData
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch
(
error
)
{
if
(
!
shouldUseConversationMock
)
{
try
{
const
mockResponse
=
await
mockFetchConversationPage
()
return
processConversationData
(
mockResponse
.
data
.
records
)
}
catch
{
// ignore and fall through to reject
}
}
return
rejectWithValue
(
'Failed to fetch conversations'
)
}
},
...
...
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