blob: 0f3c0fe4c7431a157ba5b4827886b226f847e63f [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
14const categories = [
15 '推荐','穿搭','美食','彩妆','影视',
16 '职场','情感','家居','游戏','旅行','健身'
17]
18
95630366980c1f272025-06-20 14:08:54 +080019const recommendModes = [
20 { label: '标签推荐', value: 'tag' },
22301008b86c21c2025-06-20 19:17:00 +080021 { label: '协同过滤推荐', value: 'cf' },
22 { label: '深度推荐', value: 'deep' } // 新增
95630366980c1f272025-06-20 14:08:54 +080023]
24
22301008ba662fe2025-06-20 18:10:20 +080025const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值)
95630366980c1f272025-06-20 14:08:54 +080026const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
27
TRM-codingd1cbf672025-06-18 15:15:08 +080028export default function HomeFeed() {
TRM-coding29174c22025-06-18 23:56:51 +080029 const navigate = useNavigate()
22301008ba662fe2025-06-20 18:10:20 +080030
31 // 获取当前用户ID,如果未登录则使用默认值
32 const getCurrentUserId = () => {
33 const userInfo = getUserInfo()
34 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID
35 }
TRM-codingd1cbf672025-06-18 15:15:08 +080036 const [activeCat, setActiveCat] = useState('推荐')
37 const [items, setItems] = useState([])
38 const [loading, setLoading] = useState(true)
39 const [error, setError] = useState(null)
95630366980c1f272025-06-20 14:08:54 +080040 // JWLLL 搜索推荐相关状态
41 const [search, setSearch] = useState('')
42 const [recMode, setRecMode] = useState('tag')
43 const [recCFNum, setRecCFNum] = useState(20)
44 const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
22301008e25b4b02025-06-20 22:15:31 +080045 const [userMap, setUserMap] = useState({}) // user_id: {username, nickname}
95630366980c1f272025-06-20 14:08:54 +080046
47 // JWLLL搜索推荐内容
48 const fetchSearchContent = useCallback(async (keyword = '') => {
49 setLoading(true)
50 setError(null)
51 try {
52 const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
22301008e25b4b02025-06-20 22:15:31 +080053 // 新增:拉取详情,保证和推荐一致
54 const detailed = await Promise.all(
55 (data.results || []).map(async item => {
56 try {
57 const d = await fetchPost(item.id)
58 return {
59 id: d.id,
60 title: d.title,
61 author: `作者 ${d.user_id}`,
62 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +080063 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008e25b4b02025-06-20 22:15:31 +080064 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +080065 content: d.content || '',
66 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008e25b4b02025-06-20 22:15:31 +080067 }
68 } catch {
69 return {
70 id: item.id,
71 title: item.title,
72 author: item.author || '佚名',
73 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +080074 media: item.img || '',
22301008e25b4b02025-06-20 22:15:31 +080075 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +080076 content: item.content || '',
77 mediaUrls: []
22301008e25b4b02025-06-20 22:15:31 +080078 }
79 }
80 })
81 )
82 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +080083 } catch (e) {
84 console.error('搜索失败:', e)
85 setError('搜索失败')
86 setItems([])
87 }
88 setLoading(false)
89 }, [activeCat])
95630366980c1f272025-06-20 14:08:54 +080090 // 标签推荐
91 const fetchTagRecommend = useCallback(async (tags) => {
92 setLoading(true)
93 setError(null)
94 try {
22301008ba662fe2025-06-20 18:10:20 +080095 const currentUserId = getCurrentUserId()
96 const data = await searchAPI.recommendByTags(currentUserId, tags)
22301008b86c21c2025-06-20 19:17:00 +080097 // 新增:拉取详情,保证和原始数据一致
98 const detailed = await Promise.all(
99 (data.recommendations || []).map(async item => {
100 try {
101 const d = await fetchPost(item.id)
102 return {
103 id: d.id,
104 title: d.title,
105 author: `作者 ${d.user_id}`,
106 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800107 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800108 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800109 content: d.content || '',
110 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800111 }
112 } catch {
113 // 拉详情失败时兜底
114 return {
115 id: item.id,
116 title: item.title,
117 author: item.author || '佚名',
118 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800119 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800120 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800121 content: item.content || '',
122 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800123 }
124 }
125 })
126 )
127 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800128 } catch (e) {
129 console.error('标签推荐失败:', e)
130 setError('标签推荐失败')
131 setItems([])
132 }
133 setLoading(false)
134 }, [])
95630366980c1f272025-06-20 14:08:54 +0800135 // 协同过滤推荐
136 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
137 setLoading(true)
138 setError(null)
139 try {
22301008ba662fe2025-06-20 18:10:20 +0800140 const currentUserId = getCurrentUserId()
141 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
22301008b86c21c2025-06-20 19:17:00 +0800142 // 新增:拉取详情,保证和原始数据一致
143 const detailed = await Promise.all(
144 (data.recommendations || []).map(async item => {
145 try {
146 const d = await fetchPost(item.id)
147 return {
148 id: d.id,
149 title: d.title,
150 author: `作者 ${d.user_id}`,
151 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800152 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800153 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800154 content: d.content || '',
155 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800156 }
157 } catch {
158 // 拉详情失败时兜底
159 return {
160 id: item.id,
161 title: item.title,
162 author: item.author || '佚名',
163 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800164 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800165 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800166 content: item.content || '',
167 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800168 }
169 }
170 })
171 )
172 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800173 } catch (e) {
174 console.error('协同过滤推荐失败:', e)
175 setError('协同过滤推荐失败')
176 setItems([])
177 }
178 setLoading(false)
179 }, [recCFNum])
22301008b86c21c2025-06-20 19:17:00 +0800180 // 深度推荐
181 const fetchDeepRecommend = useCallback(async (topN = 20) => {
182 setLoading(true)
183 setError(null)
184 try {
185 const currentUserId = getCurrentUserId()
186 const recs = await deepRecommend(currentUserId, topN)
187 // 拉取详情,保证和原始数据一致
188 const detailed = await Promise.all(
189 (recs || []).map(async item => {
190 try {
191 const d = await fetchPost(item.id)
192 return {
193 id: d.id,
194 title: d.title,
195 author: `作者 ${d.user_id}`,
196 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800197 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
22301008b86c21c2025-06-20 19:17:00 +0800198 likes: d.heat,
9563036699de8c092025-06-21 16:41:18 +0800199 content: d.content || '',
200 mediaUrls: d.media_urls || [] // 保存所有媒体URL
22301008b86c21c2025-06-20 19:17:00 +0800201 }
202 } catch {
203 return {
204 id: item.id,
205 title: item.title,
206 author: item.author || '佚名',
207 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
9563036699de8c092025-06-21 16:41:18 +0800208 media: item.img || '',
22301008b86c21c2025-06-20 19:17:00 +0800209 likes: item.heat || 0,
9563036699de8c092025-06-21 16:41:18 +0800210 content: item.content || '',
211 mediaUrls: []
22301008b86c21c2025-06-20 19:17:00 +0800212 }
213 }
214 })
215 )
216 setItems(detailed)
217 } catch (e) {
218 setError('深度推荐失败')
219 setItems([])
220 }
221 setLoading(false)
222 }, [])
95630366980c1f272025-06-20 14:08:54 +0800223 // 获取用户兴趣标签后再推荐
224 const fetchUserTagsAndRecommend = useCallback(async () => {
225 setLoading(true)
226 setError(null)
227 let tags = []
228 try {
22301008ba662fe2025-06-20 18:10:20 +0800229 const currentUserId = getCurrentUserId()
230 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800231 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
232 } catch {
233 tags = DEFAULT_TAGS
234 }
235 if (recMode === 'tag') {
236 await fetchTagRecommend(tags)
22301008b86c21c2025-06-20 19:17:00 +0800237 } else if (recMode === 'cf') {
95630366980c1f272025-06-20 14:08:54 +0800238 await fetchCFRecommend()
22301008b86c21c2025-06-20 19:17:00 +0800239 } else if (recMode === 'deep') {
240 await fetchDeepRecommend()
95630366980c1f272025-06-20 14:08:54 +0800241 }
242 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800243 }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800244
22301008e25b4b02025-06-20 22:15:31 +0800245 // 拉取所有涉及用户的昵称
246 const fetchUserNames = async (userIds) => {
247 const map = {}
248 await Promise.all(userIds.map(async uid => {
249 try {
250 const user = await postsAPI.getUser(uid)
251 map[uid] = user.username || user.nickname || `用户${uid}`
252 } catch {
253 map[uid] = `用户${uid}`
254 }
255 }))
256 setUserMap(map)
257 }
258
TRM-codingd1cbf672025-06-18 15:15:08 +0800259 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800260 // 原始数据加载函数
261 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800262 try {
263 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
264 // 为了拿到 media_urls 和 user_id,这里再拉详情
265 const detailed = await Promise.all(
266 list.map(async p => {
267 const d = await fetchPost(p.id)
268 return {
269 id: d.id,
270 title: d.title,
trm9984ee52025-06-20 15:16:56 +0000271 author: `作者 ${d.user_id}`,
9563036699de8c092025-06-21 16:41:18 +0800272 authorId: d.user_id,
273 avatar: `http://192.168.5.200:8080/static/profile.webp`,
274 media: d.media_urls?.[0] || '', // 改为 media,支持图片和视频
275 likes: d.heat,
276 mediaUrls: d.media_urls || [] // 保存所有媒体URL
TRM-codingd1cbf672025-06-18 15:15:08 +0800277 }
278 })
279 )
280 setItems(detailed)
22301008e25b4b02025-06-20 22:15:31 +0800281 // 拉取所有涉及用户的昵称
282 const userIds = [...new Set(detailed.map(i => i.authorId))]
283 fetchUserNames(userIds)
TRM-codingd1cbf672025-06-18 15:15:08 +0800284 } catch (e) {
285 setError(e.message)
286 } finally {
287 setLoading(false)
288 }
289 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800290
95630366980c1f272025-06-20 14:08:54 +0800291 // 根据模式选择加载方式
292 if (activeCat === '推荐' && useSearchRecommend) {
293 fetchUserTagsAndRecommend()
294 } else {
295 loadPosts()
296 }
297 }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
298 // 切换推荐模式时的额外处理
299 useEffect(() => {
300 if (activeCat === '推荐' && useSearchRecommend) {
301 fetchUserTagsAndRecommend()
302 }
303 // eslint-disable-next-line
304 }, [recMode, fetchUserTagsAndRecommend])
305
306 // 根据模式选择不同的加载方式
307 const handleSearch = e => {
308 e.preventDefault()
309 if (useSearchRecommend) {
310 fetchSearchContent(search)
311 } else {
312 // 切换到搜索推荐模式
313 setUseSearchRecommend(true)
314 fetchSearchContent(search)
315 }
316 }
317
9563036699de8c092025-06-21 16:41:18 +0800318 const [previewImg, setPreviewImg] = useState(null)
319
95630366980c1f272025-06-20 14:08:54 +0800320 const handlePostClick = (postId) => {
321 navigate(`/post/${postId}`)
322 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800323 return (
324 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800325 {/* 数据源切换 */}
326 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
327 <span>数据源:</span>
328 <div style={{display:'flex', gap:8}}>
329 <button
330 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
331 onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
332 type="button"
333 style={{
334 borderRadius: 20,
335 padding: '4px 18px',
336 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
337 background: !useSearchRecommend ? '#fff0f0' : '#fff',
338 color: !useSearchRecommend ? '#e84c4a' : '#333',
339 fontWeight: !useSearchRecommend ? 600 : 400,
340 cursor: 'pointer',
341 transition: 'all 0.2s',
342 outline: 'none',
343 }}
344 >原始数据</button>
345 <button
346 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
347 onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
348 type="button"
349 style={{
350 borderRadius: 20,
351 padding: '4px 18px',
352 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
353 background: useSearchRecommend ? '#fff0f0' : '#fff',
354 color: useSearchRecommend ? '#e84c4a' : '#333',
355 fontWeight: useSearchRecommend ? 600 : 400,
356 cursor: 'pointer',
357 transition: 'all 0.2s',
358 outline: 'none',
359 }}
360 >智能推荐</button>
361 </div>
362 </div>
363
364 {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
365 {activeCat === '推荐' && useSearchRecommend && (
366 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
367 <span style={{marginRight:8}}>推荐模式:</span>
368 <div style={{display:'flex', gap:8}}>
369 {recommendModes.map(m => (
370 <button
371 key={m.value}
372 className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
373 onClick={() => setRecMode(m.value)}
374 type="button"
375 style={{
376 borderRadius: 20,
377 padding: '4px 18px',
378 border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
379 background: recMode===m.value ? '#fff0f0' : '#fff',
380 color: recMode===m.value ? '#e84c4a' : '#333',
381 fontWeight: recMode===m.value ? 600 : 400,
382 cursor: 'pointer',
383 transition: 'all 0.2s',
384 outline: 'none',
385 }}
386 >{m.label}</button>
387 ))}
388 </div>
389 {/* 协同过滤推荐数量选择 */}
390 {recMode === 'cf' && (
391 <div style={{display:'flex',alignItems:'center',gap:4}}>
392 <span>推荐数量:</span>
393 <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'}}>
394 {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
395 </select>
396 </div>
397 )}
398 </div>
399 )}
400
401 {/* 搜索栏 */}
402 <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
403 <input
404 type="text"
405 className="search-input"
406 placeholder="搜索内容/标题/标签"
407 value={search}
408 onChange={e => setSearch(e.target.value)}
409 />
410 <button type="submit" className="search-btn">搜索</button>
411 </form>
412
TRM-codingd1cbf672025-06-18 15:15:08 +0800413 {/* 顶部分类 */}
414 <nav className="feed-tabs">
415 {categories.map(cat => (
416 <button
417 key={cat}
418 className={cat === activeCat ? 'tab active' : 'tab'}
95630366980c1f272025-06-20 14:08:54 +0800419 onClick={() => {
420 setActiveCat(cat);
421 setSearch('');
422 if (useSearchRecommend) {
423 if (cat === '推荐') {
424 fetchUserTagsAndRecommend()
425 } else {
426 fetchSearchContent()
427 }
428 }
429 }}
TRM-codingd1cbf672025-06-18 15:15:08 +0800430 >
431 {cat}
432 </button>
433 ))}
95630366980c1f272025-06-20 14:08:54 +0800434 </nav> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800435 {loading ? (
436 <div className="loading">加载中…</div>
437 ) : error ? (
438 <div className="error">加载失败:{error}</div>
439 ) : (
440 /* 瀑布流卡片区 */
441 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800442 {items.length === 0 ? (
443 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
444 ) : (
445 items.map(item => (
446 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
9563036699de8c092025-06-21 16:41:18 +0800447 {item.media && (
448 <MediaPreview
449 url={item.media}
450 alt={item.title}
451 className="card-img"
452 onClick={(url) => {
453 // 对于图片,显示预览
454 if (!url.toLowerCase().includes('video') && !url.includes('.mp4') && !url.includes('.webm')) {
455 setPreviewImg(url)
456 }
457 }}
458 style={{ cursor: 'pointer' }}
459 />
460 )}
95630366980c1f272025-06-20 14:08:54 +0800461 <h3 className="card-title">{item.title}</h3>
462 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
463 <div className="card-footer">
464 <div className="card-author">
22301008e25b4b02025-06-20 22:15:31 +0800465 <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId} />
466 <span className="username">{userMap[item.authorId] || item.authorId}</span>
95630366980c1f272025-06-20 14:08:54 +0800467 </div>
468 <div className="card-likes">
469 <ThumbsUp size={16} />
470 <span className="likes-count">{item.likes}</span>
471 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800472 </div>
473 </div>
95630366980c1f272025-06-20 14:08:54 +0800474 ))
475 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800476 </div>
477 )}
9563036699de8c092025-06-21 16:41:18 +0800478
479 {/* 图片预览弹窗 */}
480 {previewImg && (
481 <div
482 className="img-preview-mask"
483 style={{
484 position: 'fixed',
485 zIndex: 9999,
486 top: 0,
487 left: 0,
488 right: 0,
489 bottom: 0,
490 background: 'rgba(0,0,0,0.7)',
491 display: 'flex',
492 alignItems: 'center',
493 justifyContent: 'center'
494 }}
495 onClick={() => setPreviewImg(null)}
496 >
497 <img
498 src={previewImg}
499 alt="大图预览"
500 style={{
501 maxWidth: '90vw',
502 maxHeight: '90vh',
503 borderRadius: 12,
504 boxShadow: '0 4px 24px #0008'
505 }}
506 />
507 </div>
508 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800509 </div>
510 )
511}