Commit 94991ba6 by HoMeTown

feat: 历史记录列表

parent d1d231b0
......@@ -45,6 +45,7 @@
"ahooks": "^3.8.0",
"axios": "^1.7.3",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
......
import { AnimatePresence, motion } from 'framer-motion'
import { Button, Input } from '@nextui-org/react'
import { containerVariants, itemVariants, variants } from './motionAnimate'
import { variants } from './motionAnimate'
import { HistoryBarList } from './components/HistoryBarList'
import SearchIcon from '@/assets/svg/search.svg?react'
import HistoryMenuIcon from '@/assets/svg/historyMenu.svg?react'
......@@ -24,41 +25,7 @@ export const HistoryBar: React.FC<HistoryBarProps> = ({ isVisible }) => {
<Input classNames={{ inputWrapper: ['bg-white', 'data-[hover=true]:bg-[#fff]', 'group-data-[focus=true]:bg-white', 'rounded-[24px]'] }} placeholder="搜索历史记录" startContent={<SearchIcon />} />
</div>
<div className="px-[32px] flex-1 overflow-y-auto">
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="w-full flex flex-col gap-[6px]"
>
<motion.li
key="1"
custom="1"
variants={itemVariants}
initial="hidden"
animate="visible"
exit="exit"
layout
className="mt-[32px] text-[13px] text-[#B1C6D2]"
>
今日
</motion.li>
<motion.li
key="2"
custom="2"
variants={itemVariants}
initial="hidden"
animate="visible"
exit="exit"
layout
className="w-full"
>
<Button color="primary" variant="light" className="w-full text-[#333] rounded-[23px] data-[hover=true]:bg-[#E5F6FF] data-[hover=true]:text-primary">
<div className="w-full text-nowrap text-ellipsis overflow-hidden">
<span>推荐几款60周岁还能投的医疗保推荐几款60周岁还能投的医疗保</span>
</div>
</Button>
</motion.li>
</motion.ul>
<HistoryBarList />
</div>
<div className="text-[12px] border-t-solid border-t-[1px] border-t-[#82969C12] w-full h-[48px] flex items-center justify-center">
<Button className="w-full" color="primary" variant="light" startContent={<HistoryMenuIcon />}>
......
import { motion } from 'framer-motion'
import { Button } from '@nextui-org/react'
import { containerVariants, itemVariants } from '../../motionAnimate'
import { useAppSelector } from '@/store/hook'
export const HistoryBarList: React.FC = () => {
const { conversations } = useAppSelector(state => state.conversation)
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="w-full flex flex-col gap-[6px]"
>
{
conversations.map((item, index) => (
<motion.li
key={`${item.conversationId}-${index}`}
custom={index}
variants={itemVariants}
initial="hidden"
animate="visible"
exit="exit"
layout
>
{
item.conversationId
? (
<Button color="primary" variant="light" className="text-left w-full text-[#333] rounded-[23px] data-[hover=true]:bg-[#E5F6FF] data-[hover=true]:text-primary">
<div className="w-full text-nowrap text-ellipsis overflow-hidden">
<span>{item.conversationTitle}</span>
</div>
</Button>
)
: (
<div className="mt-[32px] text-[13px] text-[#B1C6D2]">{item.conversationTitle}</div>
)
}
</motion.li>
))
}
</motion.ul>
)
}
......@@ -54,7 +54,7 @@ export const itemVariants = {
y: 0,
scale: 1,
transition: {
delay: i * 0.08,
delay: i * 0.05,
type: 'spring',
stiffness: 200,
damping: 20,
......
import type React from 'react'
import { motion } from 'framer-motion'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Navbar } from '../Navbar'
import { HistoryBar } from '../HistoryBar/HistoryBar'
import styles from './MainLayout.module.less'
import { useAuth } from '@/auth/AuthContext'
import { LoginModal } from '@/components/LoginModal'
import { useAppDispatch } from '@/store/hook'
import { fetchConversations } from '@/store/conversationSlice'
interface MainLayoutProps {
children: React.ReactNode
......@@ -25,6 +27,11 @@ const contentVariants = {
export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const { showLoginModal, toggleLoginModal } = useAuth()
const [isHistoryVisible, setHistoryVisible] = useState(false)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(fetchConversations())
}, [dispatch])
return (
<motion.main className={styles.layoutMain}>
......
import { isThisWeek, isToday, isWithinInterval, subDays } from 'date-fns'
import type { Conversation } from '@/types/conversation'
export function processConversationData(records: Conversation[]): Conversation[] {
const today = new Date()
const thirtyDaysAgo = subDays(today, 30)
const processedData: Conversation[] = []
let hasToday = false
let hasThisWeek = false
let hasLast30Days = false
records.forEach((item) => {
const endDate = new Date(item.endTime)
let timeLabel = ''
if (isToday(endDate)) {
if (!hasToday) {
processedData.push({
conversationId: '',
conversationTitle: '今天',
currentConversationFlag: false,
endTime: '',
} as any)
hasToday = true
}
timeLabel = '今天'
}
else if (isThisWeek(endDate, { weekStartsOn: 1 })) {
if (!hasThisWeek) {
processedData.push({
conversationId: '',
conversationTitle: '最近一周',
currentConversationFlag: false,
endTime: '',
} as Conversation)
hasThisWeek = true
}
timeLabel = '最近一周'
}
else if (isWithinInterval(endDate, { start: thirtyDaysAgo, end: today })) {
if (!hasLast30Days) {
processedData.push({
conversationId: '',
conversationTitle: '最近30天',
currentConversationFlag: false,
endTime: '',
} as Conversation)
hasLast30Days = true
}
// eslint-disable-next-line unused-imports/no-unused-vars
timeLabel = '最近30天'
}
else {
return
}
processedData.push({
...item,
})
})
return processedData
}
import type { PayloadAction } from '@reduxjs/toolkit'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { processConversationData } from './conversationSlice.helper'
import type { Conversation, ConversationState } from '@/types/conversation'
import { queryUserConversationPage } from '@/api/conversation'
const initialState: ConversationState = {
conversations: [],
currentConversationId: null,
isLoading: false,
error: null,
}
export const fetchConversations = createAsyncThunk(
'conversation/fetchConversations',
async (_, { rejectWithValue }) => {
try {
const response = await queryUserConversationPage({
keyword: '',
pageNum: 0,
pageSize: 100,
})
const processedData = processConversationData(response.data.records)
return processedData
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (error) {
return rejectWithValue('Failed to fetch conversations')
}
},
)
const conversationSlice = createSlice({
name: 'conversation',
initialState,
reducers: {
setCurrentConversation: (state, action: PayloadAction<string>) => {
state.currentConversationId = action.payload
},
addConversation: (state, action: PayloadAction<Conversation>) => {
state.conversations.unshift(action.payload)
},
removeConversation: (state, action: PayloadAction<string>) => {
state.conversations = state.conversations.filter(conv => conv.conversationId !== action.payload)
if (state.currentConversationId === action.payload) {
state.currentConversationId = state.conversations[0]?.conversationId || null
}
},
},
extraReducers: (builder) => {
builder
.addCase(fetchConversations.pending, (state) => {
state.isLoading = true
state.error = null
})
.addCase(fetchConversations.fulfilled, (state, action) => {
state.isLoading = false
state.conversations = action.payload
})
.addCase(fetchConversations.rejected, (state, action) => {
state.isLoading = false
state.error = action.payload as string
})
},
})
export const { setCurrentConversation } = conversationSlice.actions
export default conversationSlice.reducer
import { configureStore } from '@reduxjs/toolkit'
import conversationReducer from './conversationSlice'
export const store = configureStore({
reducer: {
// 我们暂时还没有任何reducer,所以这里是空的
conversation: conversationReducer,
},
})
// 为了在TypeScript中使用,我们导出这些类型
......
export interface Conversation {
conversationId: string
conversationTitle: string
currentConversationFlag: boolean
endTime: string
startTime: string
qaNum: number
}
export interface ConversationState {
conversations: Conversation[]
currentConversationId: string | null
isLoading: boolean
error: string | null
}
......@@ -29,7 +29,7 @@ const service = axios.create({
service.interceptors.request.use(
(config: any) => {
config.headers = {
'X-Token': window.localStorage.getItem('__TOKEN__') || '',
'X-Token': JSON.parse(window.localStorage.getItem('__TOKEN__') || ''),
'X-Request-Id': `${Date.now()}${Math.random().toString(36).substring(2)}`,
'X-Request-By': config.url,
// 'X-App-Type': getAppType() || '',
......
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