Commit 80f578fa by Liu

chore: commit staged changes

parent 310b6cca
......@@ -2,43 +2,15 @@ import React, { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Button, Tooltip } from '@heroui/react'
import { useLocalStorageState, useToggle } from 'ahooks'
import { useSearchParams } from 'react-router-dom'
import { useLocation, useSearchParams } from 'react-router-dom'
import { LoginModal } from '../LoginModal'
import type { RootState } from '@/store'
import SendIcon from '@/assets/svg/send.svg?react'
import { type WithAuthProps, withAuth } from '@/auth/withAuth'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import { clearCurrentToolId, createConversation, setCurrentToolId } from '@/store/conversationSlice'
// 本地 mock 工具列表,离线/联调阶段用于渲染按钮
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
import { fetchToolList } from '@/api/home'
import { getUserRolesForApi, safeSessionStorageGetItem, safeSessionStorageRemoveItem, safeSessionStorageSetItem } from '@/lib/utils'
interface ChatEditorProps {
onChange?: (value: string) => void
......@@ -66,12 +38,25 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const [showToolQuestion, setShowToolQuestion] = useState<boolean>(false)
const [sessionToolId, setSessionToolId] = useState<string | null>(null)
const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation()
const toolIdFromUrl = searchParams.get('toolId')
// 获取工具列表
const getToolList = async () => {
// 暂时启用本地 mock 数据,确保在无后端接口时也能渲染
setToolList(MOCK_TOOL_LIST.map(tool => ({ ...tool })))
try {
// 从路由中获取 userRoles 参数
const userRoles = getUserRolesForApi()
// eslint-disable-next-line no-console
console.log('[ChatEditor] 获取到的 userRoles 参数:', userRoles, '类型:', Array.isArray(userRoles) ? 'array' : typeof userRoles)
// 调用真实 API 获取工具列表
const res = await fetchToolList({ userRoles })
if (res?.data && Array.isArray(res.data) && res.data.length > 0) {
setToolList(res.data)
}
}
catch (error) {
console.error('获取工具列表失败:', error)
}
}
// 根据 currentToolId 以及 sessionStorage 中的记录决定高亮逻辑
......@@ -128,7 +113,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
// 监听 sessionStorage 中的 currentToolId(历史点击时写入)来辅助高亮逻辑
useEffect(() => {
const syncSessionToolId = () => {
const storedToolId = sessionStorage.getItem('currentToolId')
const storedToolId = safeSessionStorageGetItem('currentToolId')
// 如果 currentToolId 是空字符串,视为 null,确保通用模式能正确高亮
setSessionToolId(storedToolId && storedToolId.trim() ? storedToolId : null)
}
......@@ -157,7 +142,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
// 当路由变化时,同步更新 sessionToolId(因为 storage 事件不会在同标签页触发)
useEffect(() => {
const storedToolId = sessionStorage.getItem('currentToolId')
const storedToolId = safeSessionStorageGetItem('currentToolId')
// 如果 currentToolId 是空字符串,视为 null,确保通用模式能正确高亮
setSessionToolId(storedToolId && storedToolId.trim() ? storedToolId : null)
}, [toolIdFromUrl])
......@@ -237,8 +222,8 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
// 立即更新本地状态,让 UI 立即响应
setIsToolBtnActive(true)
setSelectedToolId(null)
sessionStorage.removeItem('showToolQuestion')
sessionStorage.removeItem('currentToolId')
safeSessionStorageRemoveItem('showToolQuestion')
safeSessionStorageRemoveItem('currentToolId')
setSessionToolId(null)
setShowToolQuestion(false)
// 清空路由中的 toolId 参数
......@@ -262,37 +247,19 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
// 处理工具按钮点击:先创建新会话,再切换工具
const handleToolClick = async (tool: any) => {
// if (!checkAuth())
// return
// 调试:点击时记录当前与将要设置的 toolId,排查选中状态是否被覆盖
// eslint-disable-next-line no-console
console.log('[ChatEditor] handleToolClick 点击前:', {
clickToolId: tool.toolId,
toolName: tool.toolName,
selectedToolId,
sessionToolId,
currentToolIdInRedux: currentToolId,
})
if (tool.toolName === '数据助手') {
sessionStorage.setItem('showToolQuestion', 'true')
safeSessionStorageSetItem('showToolQuestion', 'true')
setShowToolQuestion(true)
}
else {
sessionStorage.removeItem('showToolQuestion')
safeSessionStorageRemoveItem('showToolQuestion')
setShowToolQuestion(false)
}
dispatch(setCurrentToolId(tool.toolId))
setSelectedToolId(tool.toolId)
setIsToolBtnActive(false)
sessionStorage.setItem('currentToolId', tool.toolId)
safeSessionStorageSetItem('currentToolId', tool.toolId)
setSessionToolId(tool.toolId)
// eslint-disable-next-line no-console
console.log('[ChatEditor] handleToolClick 点击后(已设置选中):', {
clickToolId: tool.toolId,
toolName: tool.toolName,
selectedToolIdAfterSet: tool.toolId,
sessionToolIdAfterSet: tool.toolId,
})
try {
await dispatch(createConversation({
conversationData: { toolId: tool.toolId },
......@@ -321,15 +288,15 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
}
}, [content])
// 组件加载时获取工具列表
// 组件加载时和路由参数变化时获取工具列表
useEffect(() => {
getToolList()
}, [])
}, [location.pathname, location.search])
// 监听 sessionStorage 中的 showToolQuestion
useEffect(() => {
const checkShowToolQuestion = () => {
const value = sessionStorage.getItem('showToolQuestion')
const value = safeSessionStorageGetItem('showToolQuestion')
setShowToolQuestion(value === 'true')
}
checkShowToolQuestion()
......@@ -434,17 +401,6 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
// 通用模式高亮:路由内没有 toolId 或 toolId 为空时默认高亮,点击后也要高亮
const isGeneralMode = tool.toolName === '通用模式' && isToolBtnActive && !selectedToolId && !sessionToolId && !toolIdFromUrl
const isSelected = isSelectedByState || isSelectedBySession || isSelectedByUrl || isGeneralMode
// 调试打印
if (index === 0 || selectedToolId === tool.toolId) {
// eslint-disable-next-line no-console
console.log('[ChatEditor] 按钮渲染:', {
toolName: tool.toolName,
toolId: tool.toolId,
selectedToolId,
isSelected,
isToolBtnActive,
})
}
const baseBtnClass
= 'w-auto h-[32px] px-3 rounded-full shadow-none text-[12px] flex items-center gap-2 transition-all duration-200 border'
......
......@@ -7,6 +7,73 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* 安全地访问 sessionStorage
* 在无痕模式或存储被禁用时返回 null,避免抛出错误
*/
export function safeSessionStorageGetItem(key: string): string | null {
try {
return sessionStorage.getItem(key)
}
catch {
// 在无痕模式或存储被禁用时,静默失败
return null
}
}
/**
* 安全地设置 sessionStorage
* 在无痕模式或存储被禁用时静默失败
*/
export function safeSessionStorageSetItem(key: string, value: string): void {
try {
sessionStorage.setItem(key, value)
}
catch {
// 在无痕模式或存储被禁用时,静默失败
}
}
/**
* 安全地删除 sessionStorage
* 在无痕模式或存储被禁用时静默失败
*/
export function safeSessionStorageRemoveItem(key: string): void {
try {
sessionStorage.removeItem(key)
}
catch {
// 在无痕模式或存储被禁用时,静默失败
}
}
/**
* 安全地访问 localStorage
* 在无痕模式或存储被禁用时返回 null,避免抛出错误
*/
export function safeLocalStorageGetItem(key: string): string | null {
try {
return localStorage.getItem(key)
}
catch {
// 在无痕模式或存储被禁用时,静默失败
return null
}
}
/**
* 安全地设置 localStorage
* 在无痕模式或存储被禁用时静默失败
*/
export function safeLocalStorageSetItem(key: string, value: string): void {
try {
localStorage.setItem(key, value)
}
catch {
// 在无痕模式或存储被禁用时,静默失败
}
}
const USER_ROLES_STORAGE_KEY = '__USER_ROLES__'
/**
......@@ -35,20 +102,105 @@ function getQueryBeforeSecondQuestion(): string {
}
/**
* 从路径中提取查询字符串(支持参数作为路径一部分的情况)
* 例如:/home/userRoles=00&&userRoles=01&&loginCode=2392039
* @returns 返回查询字符串,格式为 key=value&key=value
*/
function extractQueryStringFromPath(): string {
try {
const pathname = window.location.pathname || ''
// 如果路径中包含 = 符号,说明参数可能作为路径的一部分
if (pathname.includes('=')) {
// 找到最后一个 / 之后的内容
// 例如:/home/userRoles=00&&userRoles=01 提取 userRoles=00&&userRoles=01
const lastSlashIndex = pathname.lastIndexOf('/')
if (lastSlashIndex !== -1 && lastSlashIndex < pathname.length - 1) {
const pathAfterLastSlash = pathname.substring(lastSlashIndex + 1)
if (pathAfterLastSlash.includes('=')) {
// 兼容 && 作为分隔符的情况,统一替换成单个 &
return pathAfterLastSlash.replace(/&{2,}/g, '&')
}
}
}
return ''
}
catch {
return ''
}
}
/**
* 从路由获取 userRoles(不存储到 localStorage)
* 支持两种格式:
* 1. 标准查询参数:/home?userRoles=00&userRoles=01
* 2. 路径参数:/home/userRoles=00&&userRoles=01
* @returns 返回获取到的 userRoles 数组
*/
export function getUserRolesFromRoute(): string[] {
try {
// 首先尝试从标准查询参数获取
const sanitizedSearch = getQueryBeforeSecondQuestion()
const searchParams = new URLSearchParams(sanitizedSearch || window.location.search)
const rolesFromRepeatedKeys = searchParams.getAll('userRoles').filter(Boolean)
// 兜底:兼容 && 拼接
const normalizedSearch = (sanitizedSearch || window.location.search).replace(/&{2,}/g, '&')
let searchParams = new URLSearchParams(normalizedSearch)
let rolesFromRepeatedKeys = searchParams.getAll('userRoles').filter(Boolean)
let userRoles: string[] = []
// 如果从标准查询参数中找到了,直接返回
if (rolesFromRepeatedKeys.length) {
userRoles = Array.from(new Set(rolesFromRepeatedKeys))
// 首次获取到时写入 sessionStorage 兜底
try {
safeSessionStorageSetItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
catch {
// 无痕模式或被禁用时静默
}
return userRoles
}
// 如果标准查询参数中没有,尝试从路径中解析
const pathQueryString = extractQueryStringFromPath()
if (pathQueryString) {
// 将路径参数转换为标准查询字符串格式
// 例如:userRoles=00&&userRoles=01 转换为 userRoles=00&userRoles=01
const normalizedQuery = pathQueryString.replace(/&{2,}/g, '&')
searchParams = new URLSearchParams(normalizedQuery)
rolesFromRepeatedKeys = searchParams.getAll('userRoles').filter(Boolean)
if (rolesFromRepeatedKeys.length) {
userRoles = Array.from(new Set(rolesFromRepeatedKeys))
try {
safeSessionStorageSetItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
catch {
}
return userRoles
}
// 尝试逗号分隔的格式
const commaSeparated = searchParams.get('userRoles')
if (commaSeparated) {
const roles = commaSeparated
.split(',')
.map(role => role.trim())
.filter(Boolean)
if (roles.length) {
userRoles = Array.from(new Set(roles))
try {
safeSessionStorageSetItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
catch {
}
return userRoles
}
}
}
else {
// 最后尝试从标准查询参数中获取逗号分隔的格式
if (!userRoles.length) {
const commaSeparated = searchParams.get('userRoles')
if (commaSeparated) {
const roles = commaSeparated
......@@ -57,8 +209,28 @@ export function getUserRolesFromRoute(): string[] {
.filter(Boolean)
if (roles.length) {
userRoles = Array.from(new Set(roles))
try {
safeSessionStorageSetItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
catch {
}
}
}
}
// 兜底:从 sessionStorage 读取(同一标签页有效)
if (!userRoles.length) {
try {
const stored = safeSessionStorageGetItem(USER_ROLES_STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored)
if (Array.isArray(parsed) && parsed.length)
return Array.from(new Set(parsed.filter(Boolean)))
}
}
catch {
// 静默
}
}
return userRoles
......@@ -78,12 +250,7 @@ export function getUserRolesFromRouteAndStore(): string[] {
// 如果获取到了 userRoles,存储到 localStorage(向后兼容)
if (userRoles.length > 0) {
try {
localStorage.setItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
catch (error) {
console.error('存储 userRoles 到 localStorage 失败:', error)
}
safeLocalStorageSetItem(USER_ROLES_STORAGE_KEY, JSON.stringify(userRoles))
}
return userRoles
......@@ -95,7 +262,7 @@ export function getUserRolesFromRouteAndStore(): string[] {
*/
export function getUserRolesFromStorage(): string[] {
try {
const stored = localStorage.getItem(USER_ROLES_STORAGE_KEY)
const stored = safeLocalStorageGetItem(USER_ROLES_STORAGE_KEY)
if (stored) {
const userRoles = JSON.parse(stored)
if (Array.isArray(userRoles) && userRoles.length > 0) {
......@@ -103,8 +270,8 @@ export function getUserRolesFromStorage(): string[] {
}
}
}
catch (error) {
console.error('从 localStorage 读取 userRoles 失败:', error)
catch {
// 静默失败,返回空数组
}
return []
}
......
......@@ -327,6 +327,8 @@ export const Chat: React.FC = () => {
const getUserQaRecordPage = useCallback(async (conversationId: string) => {
setIsLoading(true)
try {
// 检测是否从收藏页返回
const fromCollect = location.state?.fromCollect
// eslint-disable-next-line no-console
console.log('[Chat] 开始获取历史记录:', conversationId)
const res = await fetchUserQaRecordPage(conversationId)
......@@ -384,10 +386,18 @@ export const Chat: React.FC = () => {
source: 'location.state',
})
dispatch(setCurrentToolId(toolIdFromState))
// 从收藏返回时,同步到 sessionStorage,避免 ChatEditor 清除 toolId
if (fromCollect) {
sessionStorage.setItem('currentToolId', toolIdFromState)
}
}
else {
// eslint-disable-next-line no-console
console.log('[Chat] toolId 已一致,无需更新:', toolIdFromState)
// 从收藏返回时,确保 sessionStorage 中有值
if (fromCollect && !sessionStorage.getItem('currentToolId')) {
sessionStorage.setItem('currentToolId', toolIdFromState)
}
}
}
else {
......@@ -416,10 +426,18 @@ export const Chat: React.FC = () => {
source: latestToolId ? 'qaRecords' : 'conversation',
})
dispatch(setCurrentToolId(finalToolId))
// 从收藏返回时,同步到 sessionStorage,避免 ChatEditor 清除 toolId
if (fromCollect) {
sessionStorage.setItem('currentToolId', finalToolId)
}
}
else {
// eslint-disable-next-line no-console
console.log('[Chat] toolId 已一致,无需更新:', finalToolId)
// 从收藏返回时,确保 sessionStorage 中有值
if (fromCollect && !sessionStorage.getItem('currentToolId')) {
sessionStorage.setItem('currentToolId', finalToolId)
}
}
}
else {
......@@ -435,6 +453,10 @@ export const Chat: React.FC = () => {
else if (!hasQaRecords && currentToolId) {
// eslint-disable-next-line no-console
console.log('[Chat] 没有历史记录,保留 Redux 中的 toolId (可能是 location.state 传递失败):', currentToolId)
// 从收藏返回时,确保 sessionStorage 中有值
if (fromCollect && !sessionStorage.getItem('currentToolId')) {
sessionStorage.setItem('currentToolId', currentToolId)
}
}
}
}
......@@ -445,7 +467,7 @@ export const Chat: React.FC = () => {
finally {
setIsLoading(false)
}
}, [dispatch, currentToolId, conversations])
}, [dispatch, currentToolId, conversations, location.state])
/** 点击滚动到底部 */
const scrollToBottom = () => {
......
......@@ -12,7 +12,7 @@ import { useAppDispatch } from '@/store/hook'
import { fetchEfficiencyQuestionList } from '@/api/home'
import SdreamLoading from '@/components/SdreamLoading'
import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import { getUserRolesFromRoute } from '@/lib/utils'
import { getUserRolesFromRoute, safeSessionStorageGetItem, safeSessionStorageRemoveItem } from '@/lib/utils'
function getAnimationProps(delay: number) {
return {
......@@ -86,7 +86,7 @@ export const Home: React.FC = () => {
}))
setIsDataLoaded(false) // 重置加载状态
try {
const storedToolId = sessionStorage.getItem('currentToolId') || ''
const storedToolId = safeSessionStorageGetItem('currentToolId') || ''
const searchParams = new URLSearchParams(location.search)
// 首页初始化加载常见问题时,允许忽略路由中的 toolId,避免带入上一次的工具 ID
const urlToolId = ignoreUrlToolId ? '' : (searchParams.get('toolId') || '')
......@@ -180,7 +180,7 @@ export const Home: React.FC = () => {
// 1. 清除 Redux 中的 currentToolId
dispatch(clearCurrentToolId())
// 2. 清除 sessionStorage 中的 currentToolId
sessionStorage.removeItem('currentToolId')
safeSessionStorageRemoveItem('currentToolId')
// 3. 清除 URL 中的 toolId 参数(如果存在)
const currentUrl = new URL(window.location.href)
if (currentUrl.searchParams.has('toolId')) {
......@@ -218,7 +218,7 @@ export const Home: React.FC = () => {
// 1. 清除 Redux 中的 currentToolId
dispatch(clearCurrentToolId())
// 2. 清除 sessionStorage 中的 currentToolId
sessionStorage.removeItem('currentToolId')
safeSessionStorageRemoveItem('currentToolId')
// 3. 清除 URL 中的 toolId 参数(如果存在)
const currentUrl = new URL(window.location.href)
if (currentUrl.searchParams.has('toolId')) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment