blob: b959fd971494a497f75d480b2e09371679a1df78 [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()
trm275c6002025-06-27 15:37:32 +000029 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID
30 }
22301008661ef472025-06-26 18:58:48 +080031
trm275c6002025-06-27 15:37:32 +000032 const [items, setItems] = useState([])
33 const [loading, setLoading] = useState(true)
34 const [error, setError] = useState(null)
35 // JWLLL 搜索推荐相关状态
95630366980c1f272025-06-20 14:08:54 +080036 const [search, setSearch] = useState('')
37 const [recMode, setRecMode] = useState('tag')
38 const [recCFNum, setRecCFNum] = useState(20)
trm275c6002025-06-27 15:37:32 +000039 const [useSearchRecommend, setUseSearchRecommend] = useState(false)
40 const [userMap, setUserMap] = useState({})
41
42 // 异步加载单个帖子的详细信息
43 const loadPostDetails = async (postId, index) => {
44 try {
45 const d = await fetchPost(postId)
46 setItems(prevItems => {
47 const newItems = [...prevItems]
48 if (newItems[index]) {
49 newItems[index] = {
50 ...newItems[index],
51 media: d.media_urls?.[0] || '',
52 content: d.content || '',
53 mediaUrls: d.media_urls || [],
54 author: `作者 ${d.user_id}`,
55 authorId: d.user_id,
56 avatar: `http://192.168.5.200:8080/static/profile.webp`,
57 detailsLoaded: true
58 }
59 }
60 return newItems
61 })
62 } catch (error) {
63 console.error(`加载帖子 ${postId} 详情失败:`, error)
64 }
65 }
66
67 // JWLLL搜索推荐内容
95630366980c1f272025-06-20 14:08:54 +080068 const fetchSearchContent = useCallback(async (keyword = '') => {
69 setLoading(true)
70 setError(null)
71 try {
22301008661ef472025-06-26 18:58:48 +080072 const data = await searchAPI.search(keyword, undefined)
trm275c6002025-06-27 15:37:32 +000073 // 先设置基础数据,快速显示
74 const basicItems = (data.results || []).map(item => ({
75 id: item.id,
76 title: item.title,
77 author: item.author || '佚名',
78 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
79 media: item.img || '',
80 likes: item.heat || 0,
81 content: item.content || '',
82 mediaUrls: [],
83 detailsLoaded: false
84 }))
85 setItems(basicItems)
86 setLoading(false)
87
88 // 异步加载详细信息
89 basicItems.forEach((item, index) => {
90 loadPostDetails(item.id, index)
91 })
95630366980c1f272025-06-20 14:08:54 +080092 } catch (e) {
93 console.error('搜索失败:', e)
94 setError('搜索失败')
95 setItems([])
trm275c6002025-06-27 15:37:32 +000096 setLoading(false)
95630366980c1f272025-06-20 14:08:54 +080097 }
22301008661ef472025-06-26 18:58:48 +080098 }, [])
trm275c6002025-06-27 15:37:32 +000099
95630366980c1f272025-06-20 14:08:54 +0800100 // 标签推荐
101 const fetchTagRecommend = useCallback(async (tags) => {
102 setLoading(true)
103 setError(null)
104 try {
22301008ba662fe2025-06-20 18:10:20 +0800105 const currentUserId = getCurrentUserId()
106 const data = await searchAPI.recommendByTags(currentUserId, tags)
trm275c6002025-06-27 15:37:32 +0000107 // 先设置基础数据,快速显示
108 const basicItems = (data.recommendations || []).map(item => ({
109 id: item.id,
110 title: item.title,
111 author: item.author || '佚名',
112 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
113 media: item.img || '',
114 likes: item.heat || 0,
115 content: item.content || '',
116 mediaUrls: [],
117 detailsLoaded: false
118 }))
119 setItems(basicItems)
120 setLoading(false)
121
122 // 异步加载详细信息
123 basicItems.forEach((item, index) => {
124 loadPostDetails(item.id, index)
125 })
95630366980c1f272025-06-20 14:08:54 +0800126 } catch (e) {
127 console.error('标签推荐失败:', e)
128 setError('标签推荐失败')
129 setItems([])
trm275c6002025-06-27 15:37:32 +0000130 setLoading(false)
95630366980c1f272025-06-20 14:08:54 +0800131 }
95630366980c1f272025-06-20 14:08:54 +0800132 }, [])
trm275c6002025-06-27 15:37:32 +0000133
95630366980c1f272025-06-20 14:08:54 +0800134 // 协同过滤推荐
135 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
136 setLoading(true)
137 setError(null)
138 try {
22301008ba662fe2025-06-20 18:10:20 +0800139 const currentUserId = getCurrentUserId()
140 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
trm275c6002025-06-27 15:37:32 +0000141 // 先设置基础数据,快速显示
142 const basicItems = (data.recommendations || []).map(item => ({
143 id: item.id,
144 title: item.title,
145 author: item.author || '佚名',
146 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
147 media: item.img || '',
148 likes: item.heat || 0,
149 content: item.content || '',
150 mediaUrls: [],
151 detailsLoaded: false
152 }))
153 setItems(basicItems)
154 setLoading(false)
155
156 // 异步加载详细信息
157 basicItems.forEach((item, index) => {
158 loadPostDetails(item.id, index)
159 })
95630366980c1f272025-06-20 14:08:54 +0800160 } catch (e) {
161 console.error('协同过滤推荐失败:', e)
162 setError('协同过滤推荐失败')
163 setItems([])
trm275c6002025-06-27 15:37:32 +0000164 setLoading(false)
95630366980c1f272025-06-20 14:08:54 +0800165 }
95630366980c1f272025-06-20 14:08:54 +0800166 }, [recCFNum])
trm275c6002025-06-27 15:37:32 +0000167
22301008b86c21c2025-06-20 19:17:00 +0800168 // 深度推荐
169 const fetchDeepRecommend = useCallback(async (topN = 20) => {
170 setLoading(true)
171 setError(null)
172 try {
173 const currentUserId = getCurrentUserId()
174 const recs = await deepRecommend(currentUserId, topN)
trm275c6002025-06-27 15:37:32 +0000175 // 先设置基础数据,快速显示
176 const basicItems = (recs || []).map(item => ({
177 id: item.id,
178 title: item.title,
179 author: item.author || '佚名',
180 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
181 media: item.img || '',
182 likes: item.heat || 0,
183 content: item.content || '',
184 mediaUrls: [],
185 detailsLoaded: false
186 }))
187 setItems(basicItems)
188 setLoading(false)
189
190 // 异步加载详细信息
191 basicItems.forEach((item, index) => {
192 loadPostDetails(item.id, index)
193 })
22301008b86c21c2025-06-20 19:17:00 +0800194 } catch (e) {
195 setError('深度推荐失败')
196 setItems([])
trm275c6002025-06-27 15:37:32 +0000197 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800198 }
22301008b86c21c2025-06-20 19:17:00 +0800199 }, [])
trm275c6002025-06-27 15:37:32 +0000200
95630366980c1f272025-06-20 14:08:54 +0800201 // 获取用户兴趣标签后再推荐
202 const fetchUserTagsAndRecommend = useCallback(async () => {
203 setLoading(true)
204 setError(null)
205 let tags = []
206 try {
22301008ba662fe2025-06-20 18:10:20 +0800207 const currentUserId = getCurrentUserId()
208 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800209 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
210 } catch {
211 tags = DEFAULT_TAGS
212 }
213 if (recMode === 'tag') {
214 await fetchTagRecommend(tags)
22301008b86c21c2025-06-20 19:17:00 +0800215 } else if (recMode === 'cf') {
95630366980c1f272025-06-20 14:08:54 +0800216 await fetchCFRecommend()
22301008b86c21c2025-06-20 19:17:00 +0800217 } else if (recMode === 'deep') {
218 await fetchDeepRecommend()
95630366980c1f272025-06-20 14:08:54 +0800219 }
220 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800221 }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800222
22301008e25b4b02025-06-20 22:15:31 +0800223 // 拉取所有涉及用户的昵称
224 const fetchUserNames = async (userIds) => {
225 const map = {}
226 await Promise.all(userIds.map(async uid => {
227 try {
228 const user = await postsAPI.getUser(uid)
229 map[uid] = user.username || user.nickname || `用户${uid}`
230 } catch {
231 map[uid] = `用户${uid}`
232 }
233 }))
234 setUserMap(map)
235 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800236 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800237 // 原始数据加载函数
238 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800239 try {
240 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
trm275c6002025-06-27 15:37:32 +0000241 // 先设置基础数据,快速显示
242 const basicItems = list.map(p => ({
243 id: p.id,
244 title: p.title,
245 author: '加载中...',
246 authorId: null,
247 avatar: `http://192.168.5.200:8080/static/profile.webp`,
248 media: '',
249 likes: p.heat,
250 mediaUrls: [],
251 detailsLoaded: false
252 }))
253 setItems(basicItems)
254 setLoading(false)
255
256 // 异步加载详细信息
257 basicItems.forEach((item, index) => {
258 loadPostDetails(item.id, index)
259 })
260
261 // 异步加载用户信息
262 setTimeout(async () => {
263 const allItems = await Promise.all(
264 list.map(async p => {
265 try {
266 const d = await fetchPost(p.id)
267 return { ...p, user_id: d.user_id }
268 } catch {
269 return p
270 }
271 })
272 )
273 const userIds = [...new Set(allItems.map(i => i.user_id).filter(Boolean))]
274 fetchUserNames(userIds)
275 }, 100)
TRM-codingd1cbf672025-06-18 15:15:08 +0800276 } catch (e) {
277 setError(e.message)
TRM-codingd1cbf672025-06-18 15:15:08 +0800278 setLoading(false)
279 }
280 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800281
95630366980c1f272025-06-20 14:08:54 +0800282 // 根据模式选择加载方式
22301008661ef472025-06-26 18:58:48 +0800283 if (useSearchRecommend) {
95630366980c1f272025-06-20 14:08:54 +0800284 fetchUserTagsAndRecommend()
285 } else {
286 loadPosts()
287 }
trm275c6002025-06-27 15:37:32 +0000288 }, [useSearchRecommend, fetchUserTagsAndRecommend])
95630366980c1f272025-06-20 14:08:54 +0800289 useEffect(() => {
22301008661ef472025-06-26 18:58:48 +0800290 if (useSearchRecommend) {
95630366980c1f272025-06-20 14:08:54 +0800291 fetchUserTagsAndRecommend()
292 }
293 // eslint-disable-next-line
294 }, [recMode, fetchUserTagsAndRecommend])
295
296 // 根据模式选择不同的加载方式
297 const handleSearch = e => {
298 e.preventDefault()
299 if (useSearchRecommend) {
300 fetchSearchContent(search)
301 } else {
302 // 切换到搜索推荐模式
303 setUseSearchRecommend(true)
304 fetchSearchContent(search)
305 }
306 }
307
9563036699de8c092025-06-21 16:41:18 +0800308 const [previewImg, setPreviewImg] = useState(null)
309
95630366980c1f272025-06-20 14:08:54 +0800310 const handlePostClick = (postId) => {
311 navigate(`/post/${postId}`)
312 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800313 return (
314 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800315 {/* 数据源切换 */}
316 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
317 <span>数据源:</span>
22301008661ef472025-06-26 18:58:48 +0800318 <div style={{display:'flex', gap:8}}> <button
95630366980c1f272025-06-20 14:08:54 +0800319 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
22301008661ef472025-06-26 18:58:48 +0800320 onClick={() => setUseSearchRecommend(false)}
95630366980c1f272025-06-20 14:08:54 +0800321 type="button"
322 style={{
323 borderRadius: 20,
324 padding: '4px 18px',
325 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
326 background: !useSearchRecommend ? '#fff0f0' : '#fff',
327 color: !useSearchRecommend ? '#e84c4a' : '#333',
328 fontWeight: !useSearchRecommend ? 600 : 400,
329 cursor: 'pointer',
330 transition: 'all 0.2s',
331 outline: 'none',
332 }}
22301008661ef472025-06-26 18:58:48 +0800333 >原始数据</button> <button
95630366980c1f272025-06-20 14:08:54 +0800334 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
22301008661ef472025-06-26 18:58:48 +0800335 onClick={() => setUseSearchRecommend(true)}
95630366980c1f272025-06-20 14:08:54 +0800336 type="button"
337 style={{
338 borderRadius: 20,
339 padding: '4px 18px',
340 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
341 background: useSearchRecommend ? '#fff0f0' : '#fff',
342 color: useSearchRecommend ? '#e84c4a' : '#333',
343 fontWeight: useSearchRecommend ? 600 : 400,
344 cursor: 'pointer',
345 transition: 'all 0.2s',
346 outline: 'none',
347 }}
348 >智能推荐</button>
349 </div>
22301008661ef472025-06-26 18:58:48 +0800350 </div> {/* 推荐模式切换,仅在使用智能推荐时显示 */}
351 {useSearchRecommend && (
95630366980c1f272025-06-20 14:08:54 +0800352 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
353 <span style={{marginRight:8}}>推荐模式:</span>
354 <div style={{display:'flex', gap:8}}>
355 {recommendModes.map(m => (
356 <button
357 key={m.value}
358 className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
359 onClick={() => setRecMode(m.value)}
360 type="button"
361 style={{
362 borderRadius: 20,
363 padding: '4px 18px',
364 border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
365 background: recMode===m.value ? '#fff0f0' : '#fff',
366 color: recMode===m.value ? '#e84c4a' : '#333',
367 fontWeight: recMode===m.value ? 600 : 400,
368 cursor: 'pointer',
369 transition: 'all 0.2s',
370 outline: 'none',
371 }}
372 >{m.label}</button>
373 ))}
374 </div>
375 {/* 协同过滤推荐数量选择 */}
376 {recMode === 'cf' && (
377 <div style={{display:'flex',alignItems:'center',gap:4}}>
378 <span>推荐数量:</span>
379 <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'}}>
380 {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
381 </select>
382 </div>
383 )}
384 </div>
385 )}
386
387 {/* 搜索栏 */}
388 <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
389 <input
390 type="text"
391 className="search-input"
392 placeholder="搜索内容/标题/标签"
393 value={search}
394 onChange={e => setSearch(e.target.value)}
395 />
396 <button type="submit" className="search-btn">搜索</button>
22301008661ef472025-06-26 18:58:48 +0800397 </form> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800398 {loading ? (
399 <div className="loading">加载中…</div>
400 ) : error ? (
401 <div className="error">加载失败:{error}</div>
402 ) : (
403 /* 瀑布流卡片区 */
404 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800405 {items.length === 0 ? (
406 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
407 ) : (
408 items.map(item => (
409 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
trm275c6002025-06-27 15:37:32 +0000410 {item.media ? (
9563036699de8c092025-06-21 16:41:18 +0800411 <MediaPreview
412 url={item.media}
413 alt={item.title}
414 className="card-img"
415 onClick={(url) => {
416 // 对于图片,显示预览
417 if (!url.toLowerCase().includes('video') && !url.includes('.mp4') && !url.includes('.webm')) {
418 setPreviewImg(url)
419 }
420 }}
421 style={{ cursor: 'pointer' }}
422 />
trm275c6002025-06-27 15:37:32 +0000423 ) : !item.detailsLoaded ? (
424 <div className="card-img-placeholder" style={{
425 height: '200px',
426 background: '#f5f5f5',
427 display: 'flex',
428 alignItems: 'center',
429 justifyContent: 'center',
430 color: '#999',
431 fontSize: '14px'
432 }}>
433 加载中...
434 </div>
435 ) : null}
95630366980c1f272025-06-20 14:08:54 +0800436 <h3 className="card-title">{item.title}</h3>
437 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
438 <div className="card-footer">
439 <div className="card-author">
trm275c6002025-06-27 15:37:32 +0000440 <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId || item.author} />
441 <span className="username">{userMap[item.authorId] || item.author}</span>
95630366980c1f272025-06-20 14:08:54 +0800442 </div>
443 <div className="card-likes">
444 <ThumbsUp size={16} />
445 <span className="likes-count">{item.likes}</span>
446 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800447 </div>
448 </div>
95630366980c1f272025-06-20 14:08:54 +0800449 ))
450 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800451 </div>
452 )}
9563036699de8c092025-06-21 16:41:18 +0800453
454 {/* 图片预览弹窗 */}
455 {previewImg && (
456 <div
457 className="img-preview-mask"
458 style={{
459 position: 'fixed',
460 zIndex: 9999,
461 top: 0,
462 left: 0,
463 right: 0,
464 bottom: 0,
465 background: 'rgba(0,0,0,0.7)',
466 display: 'flex',
467 alignItems: 'center',
468 justifyContent: 'center'
469 }}
470 onClick={() => setPreviewImg(null)}
471 >
472 <img
473 src={previewImg}
474 alt="大图预览"
475 style={{
476 maxWidth: '90vw',
477 maxHeight: '90vh',
478 borderRadius: 12,
479 boxShadow: '0 4px 24px #0008'
480 }}
481 />
482 </div>
483 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800484 </div>
485 )
486}