blob: e30eb73d834117b093a9b27fbf1c7ac1f1f75e05 [file] [log] [blame]
TRM-codingd1cbf672025-06-18 15:15:08 +08001// src/components/HomeFeed.jsx
2
95630366980c1f272025-06-20 14:08:54 +08003import React, { useState, useEffect, useCallback } from 'react'
TRM-coding29174c22025-06-18 23:56:51 +08004import { useNavigate } from 'react-router-dom'
TRM-codingd1cbf672025-06-18 15:15:08 +08005import { ThumbsUp } from 'lucide-react'
6import { fetchPosts, fetchPost } from '../api/posts_wzy'
95630366980c1f272025-06-20 14:08:54 +08007import { searchAPI } from '../api/search_jwlll'
22301008ba662fe2025-06-20 18:10:20 +08008import { getUserInfo } from '../utils/auth'
22301008b86c21c2025-06-20 19:17:00 +08009import { deepRecommend } from '../api/recommend_rhj'
22301008e25b4b02025-06-20 22:15:31 +080010import postsAPI from '../api/posts_api'
9563036699de8c092025-06-21 16:41:18 +080011import MediaPreview from './MediaPreview'
TRM-codingd1cbf672025-06-18 15:15:08 +080012import '../style/HomeFeed.css'
13
95630366980c1f272025-06-20 14:08:54 +080014const recommendModes = [
15 { label: '标签推荐', value: 'tag' },
22301008b86c21c2025-06-20 19:17:00 +080016 { label: '协同过滤推荐', value: 'cf' },
17 { label: '深度推荐', value: 'deep' } // 新增
95630366980c1f272025-06-20 14:08:54 +080018]
19
22301008ba662fe2025-06-20 18:10:20 +080020const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值)
95630366980c1f272025-06-20 14:08:54 +080021const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
22
TRM-codingd1cbf672025-06-18 15:15:08 +080023export default function HomeFeed() {
TRM-coding29174c22025-06-18 23:56:51 +080024 const navigate = useNavigate()
22301008ba662fe2025-06-20 18:10:20 +080025
26 // 获取当前用户ID,如果未登录则使用默认值
27 const getCurrentUserId = () => {
28 const userInfo = getUserInfo()
22301008661ef472025-06-26 18:58:48 +080029 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID }
30
TRM-codingd1cbf672025-06-18 15:15:08 +080031 const [items, setItems] = useState([])
32 const [loading, setLoading] = useState(true)
33 const [error, setError] = useState(null)
95630366980c1f272025-06-20 14:08:54 +080034 // JWLLL 搜索推荐相关状态
35 const [search, setSearch] = useState('')
36 const [recMode, setRecMode] = useState('tag')
37 const [recCFNum, setRecCFNum] = useState(20)
38 const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
22301008e25b4b02025-06-20 22:15:31 +080039 const [userMap, setUserMap] = useState({}) // user_id: {username, nickname}
22301008661ef472025-06-26 18:58:48 +080040 // JWLLL搜索推荐内容
95630366980c1f272025-06-20 14:08:54 +080041 const fetchSearchContent = useCallback(async (keyword = '') => {
42 setLoading(true)
43 setError(null)
44 try {
22301008661ef472025-06-26 18:58:48 +080045 const data = await searchAPI.search(keyword, undefined)
22301008e25b4b02025-06-20 22:15:31 +080046 // 新增:拉取详情,保证和推荐一致
47 const detailed = await Promise.all(
48 (data.results || []).map(async item => {
49 try {
50 const d = await fetchPost(item.id)
51 return {
52 id: d.id,
53 title: d.title,
54 author: `作者 ${d.user_id}`,
55 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +080056 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008e25b4b02025-06-20 22:15:31 +080057 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +080058 content: d.content || '',
59 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008e25b4b02025-06-20 22:15:31 +080060 }
61 } catch {
62 return {
63 id: item.id,
64 title: item.title,
65 author: item.author || '佚名',
66 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +080067 media: item.img || '',
22301008e25b4b02025-06-20 22:15:31 +080068 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +080069 content: item.content || '',
70 mediaUrls: []
22301008e25b4b02025-06-20 22:15:31 +080071 }
72 }
73 })
74 )
75 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +080076 } catch (e) {
77 console.error('搜索失败:', e)
78 setError('搜索失败')
79 setItems([])
80 }
81 setLoading(false)
22301008661ef472025-06-26 18:58:48 +080082 }, [])
95630366980c1f272025-06-20 14:08:54 +080083 // 标签推荐
84 const fetchTagRecommend = useCallback(async (tags) => {
85 setLoading(true)
86 setError(null)
87 try {
22301008ba662fe2025-06-20 18:10:20 +080088 const currentUserId = getCurrentUserId()
89 const data = await searchAPI.recommendByTags(currentUserId, tags)
22301008b86c21c2025-06-20 19:17:00 +080090 // 新增:拉取详情,保证和原始数据一致
91 const detailed = await Promise.all(
92 (data.recommendations || []).map(async item => {
93 try {
94 const d = await fetchPost(item.id)
95 return {
96 id: d.id,
97 title: d.title,
98 author: `作者 ${d.user_id}`,
99 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800100 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800101 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800102 content: d.content || '',
103 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800104 }
105 } catch {
106 // 拉详情失败时兜底
107 return {
108 id: item.id,
109 title: item.title,
110 author: item.author || '佚名',
111 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800112 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800113 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800114 content: item.content || '',
115 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800116 }
117 }
118 })
119 )
120 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800121 } catch (e) {
122 console.error('标签推荐失败:', e)
123 setError('标签推荐失败')
124 setItems([])
125 }
126 setLoading(false)
127 }, [])
95630366980c1f272025-06-20 14:08:54 +0800128 // 协同过滤推荐
129 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
130 setLoading(true)
131 setError(null)
132 try {
22301008ba662fe2025-06-20 18:10:20 +0800133 const currentUserId = getCurrentUserId()
134 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
22301008b86c21c2025-06-20 19:17:00 +0800135 // 新增:拉取详情,保证和原始数据一致
136 const detailed = await Promise.all(
137 (data.recommendations || []).map(async item => {
138 try {
139 const d = await fetchPost(item.id)
140 return {
141 id: d.id,
142 title: d.title,
143 author: `作者 ${d.user_id}`,
144 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800145 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800146 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800147 content: d.content || '',
148 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800149 }
150 } catch {
151 // 拉详情失败时兜底
152 return {
153 id: item.id,
154 title: item.title,
155 author: item.author || '佚名',
156 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800157 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800158 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800159 content: item.content || '',
160 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800161 }
162 }
163 })
164 )
165 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800166 } catch (e) {
167 console.error('协同过滤推荐失败:', e)
168 setError('协同过滤推荐失败')
169 setItems([])
170 }
171 setLoading(false)
172 }, [recCFNum])
22301008b86c21c2025-06-20 19:17:00 +0800173 // 深度推荐
174 const fetchDeepRecommend = useCallback(async (topN = 20) => {
175 setLoading(true)
176 setError(null)
177 try {
178 const currentUserId = getCurrentUserId()
179 const recs = await deepRecommend(currentUserId, topN)
180 // 拉取详情,保证和原始数据一致
181 const detailed = await Promise.all(
182 (recs || []).map(async item => {
183 try {
184 const d = await fetchPost(item.id)
185 return {
186 id: d.id,
187 title: d.title,
188 author: `作者 ${d.user_id}`,
189 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800190 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800191 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800192 content: d.content || '',
193 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800194 }
195 } catch {
196 return {
197 id: item.id,
198 title: item.title,
199 author: item.author || '佚名',
200 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800201 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800202 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800203 content: item.content || '',
204 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800205 }
206 }
207 })
208 )
209 setItems(detailed)
210 } catch (e) {
211 setError('深度推荐失败')
212 setItems([])
213 }
214 setLoading(false)
215 }, [])
95630366980c1f272025-06-20 14:08:54 +0800216 // 获取用户兴趣标签后再推荐
217 const fetchUserTagsAndRecommend = useCallback(async () => {
218 setLoading(true)
219 setError(null)
220 let tags = []
221 try {
22301008ba662fe2025-06-20 18:10:20 +0800222 const currentUserId = getCurrentUserId()
223 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800224 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
225 } catch {
226 tags = DEFAULT_TAGS
227 }
228 if (recMode === 'tag') {
229 await fetchTagRecommend(tags)
22301008b86c21c2025-06-20 19:17:00 +0800230 } else if (recMode === 'cf') {
95630366980c1f272025-06-20 14:08:54 +0800231 await fetchCFRecommend()
22301008b86c21c2025-06-20 19:17:00 +0800232 } else if (recMode === 'deep') {
233 await fetchDeepRecommend()
95630366980c1f272025-06-20 14:08:54 +0800234 }
235 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800236 }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800237
22301008e25b4b02025-06-20 22:15:31 +0800238 // 拉取所有涉及用户的昵称
239 const fetchUserNames = async (userIds) => {
240 const map = {}
241 await Promise.all(userIds.map(async uid => {
242 try {
243 const user = await postsAPI.getUser(uid)
244 map[uid] = user.username || user.nickname || `用户${uid}`
245 } catch {
246 map[uid] = `用户${uid}`
247 }
248 }))
249 setUserMap(map)
250 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800251 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800252 // 原始数据加载函数
253 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800254 try {
255 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
256 // 为了拿到 media_urls 和 user_id,这里再拉详情
257 const detailed = await Promise.all(
258 list.map(async p => {
259 const d = await fetchPost(p.id)
260 return {
261 id: d.id,
262 title: d.title,
trm9984ee52025-06-20 15:16:56 +0000263 author: `作者 ${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800264 authorId: d.user_id,
265 avatar: `http://192.168.5.200:8080/static/profile.webp`,
266 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
267 likes: d.heat,
268 mediaUrls: d.media_urls || [] // 保存所有媒体URL
TRM-codingd1cbf672025-06-18 15:15:08 +0800269 }
270 })
271 )
272 setItems(detailed)
22301008e25b4b02025-06-20 22:15:31 +0800273 // 拉取所有涉及用户的昵称
274 const userIds = [...new Set(detailed.map(i => i.authorId))]
275 fetchUserNames(userIds)
TRM-codingd1cbf672025-06-18 15:15:08 +0800276 } catch (e) {
277 setError(e.message)
278 } finally {
279 setLoading(false)
280 }
281 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800282
95630366980c1f272025-06-20 14:08:54 +0800283 // 根据模式选择加载方式
22301008661ef472025-06-26 18:58:48 +0800284 if (useSearchRecommend) {
95630366980c1f272025-06-20 14:08:54 +0800285 fetchUserTagsAndRecommend()
286 } else {
287 loadPosts()
288 }
22301008661ef472025-06-26 18:58:48 +0800289 }, [useSearchRecommend, fetchUserTagsAndRecommend]) // 切换推荐模式时的额外处理
95630366980c1f272025-06-20 14:08:54 +0800290 useEffect(() => {
22301008661ef472025-06-26 18:58:48 +0800291 if (useSearchRecommend) {
95630366980c1f272025-06-20 14:08:54 +0800292 fetchUserTagsAndRecommend()
293 }
294 // eslint-disable-next-line
295 }, [recMode, fetchUserTagsAndRecommend])
296
297 // 根据模式选择不同的加载方式
298 const handleSearch = e => {
299 e.preventDefault()
300 if (useSearchRecommend) {
301 fetchSearchContent(search)
302 } else {
303 // 切换到搜索推荐模式
304 setUseSearchRecommend(true)
305 fetchSearchContent(search)
306 }
307 }
308
9563036699de8c092025-06-21 16:41:18 +0800309 const [previewImg, setPreviewImg] = useState(null)
310
95630366980c1f272025-06-20 14:08:54 +0800311 const handlePostClick = (postId) => {
312 navigate(`/post/${postId}`)
313 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800314 return (
315 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800316 {/* 数据源切换 */}
317 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
318 <span>数据源:</span>
22301008661ef472025-06-26 18:58:48 +0800319 <div style={{display:'flex', gap:8}}> <button
95630366980c1f272025-06-20 14:08:54 +0800320 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
22301008661ef472025-06-26 18:58:48 +0800321 onClick={() => setUseSearchRecommend(false)}
95630366980c1f272025-06-20 14:08:54 +0800322 type="button"
323 style={{
324 borderRadius: 20,
325 padding: '4px 18px',
326 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
327 background: !useSearchRecommend ? '#fff0f0' : '#fff',
328 color: !useSearchRecommend ? '#e84c4a' : '#333',
329 fontWeight: !useSearchRecommend ? 600 : 400,
330 cursor: 'pointer',
331 transition: 'all 0.2s',
332 outline: 'none',
333 }}
22301008661ef472025-06-26 18:58:48 +0800334 >原始数据</button> <button
95630366980c1f272025-06-20 14:08:54 +0800335 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
22301008661ef472025-06-26 18:58:48 +0800336 onClick={() => setUseSearchRecommend(true)}
95630366980c1f272025-06-20 14:08:54 +0800337 type="button"
338 style={{
339 borderRadius: 20,
340 padding: '4px 18px',
341 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
342 background: useSearchRecommend ? '#fff0f0' : '#fff',
343 color: useSearchRecommend ? '#e84c4a' : '#333',
344 fontWeight: useSearchRecommend ? 600 : 400,
345 cursor: 'pointer',
346 transition: 'all 0.2s',
347 outline: 'none',
348 }}
349 >智能推荐</button>
350 </div>
22301008661ef472025-06-26 18:58:48 +0800351 </div> {/* 推荐模式切换,仅在使用智能推荐时显示 */}
352 {useSearchRecommend && (
95630366980c1f272025-06-20 14:08:54 +0800353 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
354 <span style={{marginRight:8}}>推荐模式:</span>
355 <div style={{display:'flex', gap:8}}>
356 {recommendModes.map(m => (
357 <button
358 key={m.value}
359 className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
360 onClick={() => setRecMode(m.value)}
361 type="button"
362 style={{
363 borderRadius: 20,
364 padding: '4px 18px',
365 border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
366 background: recMode===m.value ? '#fff0f0' : '#fff',
367 color: recMode===m.value ? '#e84c4a' : '#333',
368 fontWeight: recMode===m.value ? 600 : 400,
369 cursor: 'pointer',
370 transition: 'all 0.2s',
371 outline: 'none',
372 }}
373 >{m.label}</button>
374 ))}
375 </div>
376 {/* 协同过滤推荐数量选择 */}
377 {recMode === 'cf' && (
378 <div style={{display:'flex',alignItems:'center',gap:4}}>
379 <span>推荐数量:</span>
380 <select value={recCFNum} onChange={e => { setRecCFNum(Number(e.target.value)); fetchCFRecommend(Number(e.target.value)) }} style={{padding:'2px 8px',borderRadius:6,border:'1px solid #ccc'}}>
381 {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
382 </select>
383 </div>
384 )}
385 </div>
386 )}
387
388 {/* 搜索栏 */}
389 <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
390 <input
391 type="text"
392 className="search-input"
393 placeholder="搜索内容/标题/标签"
394 value={search}
395 onChange={e => setSearch(e.target.value)}
396 />
397 <button type="submit" className="search-btn">搜索</button>
22301008661ef472025-06-26 18:58:48 +0800398 </form> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800399 {loading ? (
400 <div className="loading">加载中…</div>
401 ) : error ? (
402 <div className="error">加载失败:{error}</div>
403 ) : (
404 /* 瀑布流卡片区 */
405 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800406 {items.length === 0 ? (
407 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
408 ) : (
409 items.map(item => (
410 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
9563036699de8c092025-06-21 16:41:18 +0800411 {item.media && (
412 <MediaPreview
413 url={item.media}
414 alt={item.title}
415 className="card-img"
416 onClick={(url) => {
417 // 对于图片,显示预览
418 if (!url.toLowerCase().includes('video') && !url.includes('.mp4') && !url.includes('.webm')) {
419 setPreviewImg(url)
420 }
421 }}
422 style={{ cursor: 'pointer' }}
423 />
424 )}
95630366980c1f272025-06-20 14:08:54 +0800425 <h3 className="card-title">{item.title}</h3>
426 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
427 <div className="card-footer">
428 <div className="card-author">
22301008e25b4b02025-06-20 22:15:31 +0800429 <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId} />
430 <span className="username">{userMap[item.authorId] || item.authorId}</span>
95630366980c1f272025-06-20 14:08:54 +0800431 </div>
432 <div className="card-likes">
433 <ThumbsUp size={16} />
434 <span className="likes-count">{item.likes}</span>
435 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800436 </div>
437 </div>
95630366980c1f272025-06-20 14:08:54 +0800438 ))
439 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800440 </div>
441 )}
9563036699de8c092025-06-21 16:41:18 +0800442
443 {/* 图片预览弹窗 */}
444 {previewImg && (
445 <div
446 className="img-preview-mask"
447 style={{
448 position: 'fixed',
449 zIndex: 9999,
450 top: 0,
451 left: 0,
452 right: 0,
453 bottom: 0,
454 background: 'rgba(0,0,0,0.7)',
455 display: 'flex',
456 alignItems: 'center',
457 justifyContent: 'center'
458 }}
459 onClick={() => setPreviewImg(null)}
460 >
461 <img
462 src={previewImg}
463 alt="大图预览"
464 style={{
465 maxWidth: '90vw',
466 maxHeight: '90vh',
467 borderRadius: 12,
468 boxShadow: '0 4px 24px #0008'
469 }}
470 />
471 </div>
472 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800473 </div>
474 )
475}