blob: 82e027a019dda7c56dc4269680d2e2e30c155f60 [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'
TRM-codingd1cbf672025-06-18 15:15:08 +080011import '../style/HomeFeed.css'
12
13const categories = [
14 '推荐','穿搭','美食','彩妆','影视',
15 '职场','情感','家居','游戏','旅行','健身'
16]
17
95630366980c1f272025-06-20 14:08:54 +080018const recommendModes = [
19 { label: '标签推荐', value: 'tag' },
22301008b86c21c2025-06-20 19:17:00 +080020 { label: '协同过滤推荐', value: 'cf' },
21 { label: '深度推荐', value: 'deep' } // 新增
95630366980c1f272025-06-20 14:08:54 +080022]
23
22301008ba662fe2025-06-20 18:10:20 +080024const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值)
95630366980c1f272025-06-20 14:08:54 +080025const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
26
TRM-codingd1cbf672025-06-18 15:15:08 +080027export default function HomeFeed() {
TRM-coding29174c22025-06-18 23:56:51 +080028 const navigate = useNavigate()
22301008ba662fe2025-06-20 18:10:20 +080029
30 // 获取当前用户ID,如果未登录则使用默认值
31 const getCurrentUserId = () => {
32 const userInfo = getUserInfo()
33 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID
34 }
TRM-codingd1cbf672025-06-18 15:15:08 +080035 const [activeCat, setActiveCat] = useState('推荐')
36 const [items, setItems] = useState([])
37 const [loading, setLoading] = useState(true)
38 const [error, setError] = useState(null)
95630366980c1f272025-06-20 14:08:54 +080039 // JWLLL 搜索推荐相关状态
40 const [search, setSearch] = useState('')
41 const [recMode, setRecMode] = useState('tag')
42 const [recCFNum, setRecCFNum] = useState(20)
43 const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
22301008e25b4b02025-06-20 22:15:31 +080044 const [userMap, setUserMap] = useState({}) // user_id: {username, nickname}
95630366980c1f272025-06-20 14:08:54 +080045
46 // JWLLL搜索推荐内容
47 const fetchSearchContent = useCallback(async (keyword = '') => {
48 setLoading(true)
49 setError(null)
50 try {
51 const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
22301008e25b4b02025-06-20 22:15:31 +080052 // 新增:拉取详情,保证和推荐一致
53 const detailed = await Promise.all(
54 (data.results || []).map(async item => {
55 try {
56 const d = await fetchPost(item.id)
57 return {
58 id: d.id,
59 title: d.title,
60 author: `作者 ${d.user_id}`,
61 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
62 img: d.media_urls?.[0] || '',
63 likes: d.heat,
64 content: d.content || ''
65 }
66 } catch {
67 return {
68 id: item.id,
69 title: item.title,
70 author: item.author || '佚名',
71 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
72 img: item.img || '',
73 likes: item.heat || 0,
74 content: item.content || ''
75 }
76 }
77 })
78 )
79 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +080080 } catch (e) {
81 console.error('搜索失败:', e)
82 setError('搜索失败')
83 setItems([])
84 }
85 setLoading(false)
86 }, [activeCat])
95630366980c1f272025-06-20 14:08:54 +080087 // 标签推荐
88 const fetchTagRecommend = useCallback(async (tags) => {
89 setLoading(true)
90 setError(null)
91 try {
22301008ba662fe2025-06-20 18:10:20 +080092 const currentUserId = getCurrentUserId()
93 const data = await searchAPI.recommendByTags(currentUserId, tags)
22301008b86c21c2025-06-20 19:17:00 +080094 // 新增:拉取详情,保证和原始数据一致
95 const detailed = await Promise.all(
96 (data.recommendations || []).map(async item => {
97 try {
98 const d = await fetchPost(item.id)
99 return {
100 id: d.id,
101 title: d.title,
102 author: `作者 ${d.user_id}`,
103 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
104 img: d.media_urls?.[0] || '',
105 likes: d.heat,
106 content: d.content || ''
107 }
108 } catch {
109 // 拉详情失败时兜底
110 return {
111 id: item.id,
112 title: item.title,
113 author: item.author || '佚名',
114 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
115 img: item.img || '',
116 likes: item.heat || 0,
117 content: item.content || ''
118 }
119 }
120 })
121 )
122 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800123 } catch (e) {
124 console.error('标签推荐失败:', e)
125 setError('标签推荐失败')
126 setItems([])
127 }
128 setLoading(false)
129 }, [])
95630366980c1f272025-06-20 14:08:54 +0800130 // 协同过滤推荐
131 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
132 setLoading(true)
133 setError(null)
134 try {
22301008ba662fe2025-06-20 18:10:20 +0800135 const currentUserId = getCurrentUserId()
136 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
22301008b86c21c2025-06-20 19:17:00 +0800137 // 新增:拉取详情,保证和原始数据一致
138 const detailed = await Promise.all(
139 (data.recommendations || []).map(async item => {
140 try {
141 const d = await fetchPost(item.id)
142 return {
143 id: d.id,
144 title: d.title,
145 author: `作者 ${d.user_id}`,
146 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
147 img: d.media_urls?.[0] || '',
148 likes: d.heat,
149 content: d.content || ''
150 }
151 } catch {
152 // 拉详情失败时兜底
153 return {
154 id: item.id,
155 title: item.title,
156 author: item.author || '佚名',
157 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
158 img: item.img || '',
159 likes: item.heat || 0,
160 content: item.content || ''
161 }
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}`,
190 img: d.media_urls?.[0] || '',
191 likes: d.heat,
192 content: d.content || ''
193 }
194 } catch {
195 return {
196 id: item.id,
197 title: item.title,
198 author: item.author || '佚名',
199 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
200 img: item.img || '',
201 likes: item.heat || 0,
202 content: item.content || ''
203 }
204 }
205 })
206 )
207 setItems(detailed)
208 } catch (e) {
209 setError('深度推荐失败')
210 setItems([])
211 }
212 setLoading(false)
213 }, [])
95630366980c1f272025-06-20 14:08:54 +0800214 // 获取用户兴趣标签后再推荐
215 const fetchUserTagsAndRecommend = useCallback(async () => {
216 setLoading(true)
217 setError(null)
218 let tags = []
219 try {
22301008ba662fe2025-06-20 18:10:20 +0800220 const currentUserId = getCurrentUserId()
221 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800222 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
223 } catch {
224 tags = DEFAULT_TAGS
225 }
226 if (recMode === 'tag') {
227 await fetchTagRecommend(tags)
22301008b86c21c2025-06-20 19:17:00 +0800228 } else if (recMode === 'cf') {
95630366980c1f272025-06-20 14:08:54 +0800229 await fetchCFRecommend()
22301008b86c21c2025-06-20 19:17:00 +0800230 } else if (recMode === 'deep') {
231 await fetchDeepRecommend()
95630366980c1f272025-06-20 14:08:54 +0800232 }
233 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800234 }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800235
22301008e25b4b02025-06-20 22:15:31 +0800236 // 拉取所有涉及用户的昵称
237 const fetchUserNames = async (userIds) => {
238 const map = {}
239 await Promise.all(userIds.map(async uid => {
240 try {
241 const user = await postsAPI.getUser(uid)
242 map[uid] = user.username || user.nickname || `用户${uid}`
243 } catch {
244 map[uid] = `用户${uid}`
245 }
246 }))
247 setUserMap(map)
248 }
249
TRM-codingd1cbf672025-06-18 15:15:08 +0800250 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800251 // 原始数据加载函数
252 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800253 try {
254 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
255 // 为了拿到 media_urls 和 user_id,这里再拉详情
256 const detailed = await Promise.all(
257 list.map(async p => {
258 const d = await fetchPost(p.id)
259 return {
260 id: d.id,
261 title: d.title,
trm9984ee52025-06-20 15:16:56 +0000262 author: `作者 ${d.user_id}`,
263 // avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
TRM-codingd1cbf672025-06-18 15:15:08 +0800264 img: d.media_urls?.[0] || '', // 用第一张媒体作为封面
265 likes: d.heat
266 }
267 })
268 )
269 setItems(detailed)
22301008e25b4b02025-06-20 22:15:31 +0800270 // 拉取所有涉及用户的昵称
271 const userIds = [...new Set(detailed.map(i => i.authorId))]
272 fetchUserNames(userIds)
TRM-codingd1cbf672025-06-18 15:15:08 +0800273 } catch (e) {
274 setError(e.message)
275 } finally {
276 setLoading(false)
277 }
278 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800279
95630366980c1f272025-06-20 14:08:54 +0800280 // 根据模式选择加载方式
281 if (activeCat === '推荐' && useSearchRecommend) {
282 fetchUserTagsAndRecommend()
283 } else {
284 loadPosts()
285 }
286 }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
287 // 切换推荐模式时的额外处理
288 useEffect(() => {
289 if (activeCat === '推荐' && useSearchRecommend) {
290 fetchUserTagsAndRecommend()
291 }
292 // eslint-disable-next-line
293 }, [recMode, fetchUserTagsAndRecommend])
294
295 // 根据模式选择不同的加载方式
296 const handleSearch = e => {
297 e.preventDefault()
298 if (useSearchRecommend) {
299 fetchSearchContent(search)
300 } else {
301 // 切换到搜索推荐模式
302 setUseSearchRecommend(true)
303 fetchSearchContent(search)
304 }
305 }
306
307 const handlePostClick = (postId) => {
308 navigate(`/post/${postId}`)
309 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800310 return (
311 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800312 {/* 数据源切换 */}
313 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
314 <span>数据源:</span>
315 <div style={{display:'flex', gap:8}}>
316 <button
317 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
318 onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
319 type="button"
320 style={{
321 borderRadius: 20,
322 padding: '4px 18px',
323 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
324 background: !useSearchRecommend ? '#fff0f0' : '#fff',
325 color: !useSearchRecommend ? '#e84c4a' : '#333',
326 fontWeight: !useSearchRecommend ? 600 : 400,
327 cursor: 'pointer',
328 transition: 'all 0.2s',
329 outline: 'none',
330 }}
331 >原始数据</button>
332 <button
333 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
334 onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
335 type="button"
336 style={{
337 borderRadius: 20,
338 padding: '4px 18px',
339 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
340 background: useSearchRecommend ? '#fff0f0' : '#fff',
341 color: useSearchRecommend ? '#e84c4a' : '#333',
342 fontWeight: useSearchRecommend ? 600 : 400,
343 cursor: 'pointer',
344 transition: 'all 0.2s',
345 outline: 'none',
346 }}
347 >智能推荐</button>
348 </div>
349 </div>
350
351 {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
352 {activeCat === '推荐' && useSearchRecommend && (
353 <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>
398 </form>
399
TRM-codingd1cbf672025-06-18 15:15:08 +0800400 {/* 顶部分类 */}
401 <nav className="feed-tabs">
402 {categories.map(cat => (
403 <button
404 key={cat}
405 className={cat === activeCat ? 'tab active' : 'tab'}
95630366980c1f272025-06-20 14:08:54 +0800406 onClick={() => {
407 setActiveCat(cat);
408 setSearch('');
409 if (useSearchRecommend) {
410 if (cat === '推荐') {
411 fetchUserTagsAndRecommend()
412 } else {
413 fetchSearchContent()
414 }
415 }
416 }}
TRM-codingd1cbf672025-06-18 15:15:08 +0800417 >
418 {cat}
419 </button>
420 ))}
95630366980c1f272025-06-20 14:08:54 +0800421 </nav> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800422 {loading ? (
423 <div className="loading">加载中…</div>
424 ) : error ? (
425 <div className="error">加载失败:{error}</div>
426 ) : (
427 /* 瀑布流卡片区 */
428 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800429 {items.length === 0 ? (
430 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
431 ) : (
432 items.map(item => (
433 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
434 {item.img && <img className="card-img" src={item.img} alt={item.title} />}
435 <h3 className="card-title">{item.title}</h3>
436 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
437 <div className="card-footer">
438 <div className="card-author">
22301008e25b4b02025-06-20 22:15:31 +0800439 <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId} />
440 <span className="username">{userMap[item.authorId] || item.authorId}</span>
95630366980c1f272025-06-20 14:08:54 +0800441 </div>
442 <div className="card-likes">
443 <ThumbsUp size={16} />
444 <span className="likes-count">{item.likes}</span>
445 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800446 </div>
447 </div>
95630366980c1f272025-06-20 14:08:54 +0800448 ))
449 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800450 </div>
451 )}
452 </div>
453 )
454}