blob: 1fb12a53b54d55b3aac00a8c0e04432a96aaff66 [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'
TRM-codingd1cbf672025-06-18 15:15:08 +080010import '../style/HomeFeed.css'
11
12const categories = [
13 '推荐','穿搭','美食','彩妆','影视',
14 '职场','情感','家居','游戏','旅行','健身'
15]
16
95630366980c1f272025-06-20 14:08:54 +080017const recommendModes = [
18 { label: '标签推荐', value: 'tag' },
22301008b86c21c2025-06-20 19:17:00 +080019 { label: '协同过滤推荐', value: 'cf' },
20 { label: '深度推荐', value: 'deep' } // 新增
95630366980c1f272025-06-20 14:08:54 +080021]
22
22301008ba662fe2025-06-20 18:10:20 +080023const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值)
95630366980c1f272025-06-20 14:08:54 +080024const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
25
TRM-codingd1cbf672025-06-18 15:15:08 +080026export default function HomeFeed() {
TRM-coding29174c22025-06-18 23:56:51 +080027 const navigate = useNavigate()
22301008ba662fe2025-06-20 18:10:20 +080028
29 // 获取当前用户ID,如果未登录则使用默认值
30 const getCurrentUserId = () => {
31 const userInfo = getUserInfo()
32 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID
33 }
TRM-codingd1cbf672025-06-18 15:15:08 +080034 const [activeCat, setActiveCat] = useState('推荐')
35 const [items, setItems] = useState([])
36 const [loading, setLoading] = useState(true)
37 const [error, setError] = useState(null)
95630366980c1f272025-06-20 14:08:54 +080038 // JWLLL 搜索推荐相关状态
39 const [search, setSearch] = useState('')
40 const [recMode, setRecMode] = useState('tag')
41 const [recCFNum, setRecCFNum] = useState(20)
42 const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
43
44 // JWLLL搜索推荐内容
45 const fetchSearchContent = useCallback(async (keyword = '') => {
46 setLoading(true)
47 setError(null)
48 try {
49 const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
50 const formattedItems = (data.results || []).map(item => ({
51 id: item.id,
52 title: item.title,
53 author: item.author || '佚名',
54 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
55 img: item.img || '',
56 likes: item.heat || 0,
57 content: item.content
58 }))
59 setItems(formattedItems)
60 } catch (e) {
61 console.error('搜索失败:', e)
62 setError('搜索失败')
63 setItems([])
64 }
65 setLoading(false)
66 }, [activeCat])
95630366980c1f272025-06-20 14:08:54 +080067 // 标签推荐
68 const fetchTagRecommend = useCallback(async (tags) => {
69 setLoading(true)
70 setError(null)
71 try {
22301008ba662fe2025-06-20 18:10:20 +080072 const currentUserId = getCurrentUserId()
73 const data = await searchAPI.recommendByTags(currentUserId, tags)
22301008b86c21c2025-06-20 19:17:00 +080074 // 新增:拉取详情,保证和原始数据一致
75 const detailed = await Promise.all(
76 (data.recommendations || []).map(async item => {
77 try {
78 const d = await fetchPost(item.id)
79 return {
80 id: d.id,
81 title: d.title,
82 author: `作者 ${d.user_id}`,
83 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
84 img: d.media_urls?.[0] || '',
85 likes: d.heat,
86 content: d.content || ''
87 }
88 } catch {
89 // 拉详情失败时兜底
90 return {
91 id: item.id,
92 title: item.title,
93 author: item.author || '佚名',
94 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
95 img: item.img || '',
96 likes: item.heat || 0,
97 content: item.content || ''
98 }
99 }
100 })
101 )
102 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800103 } catch (e) {
104 console.error('标签推荐失败:', e)
105 setError('标签推荐失败')
106 setItems([])
107 }
108 setLoading(false)
109 }, [])
95630366980c1f272025-06-20 14:08:54 +0800110 // 协同过滤推荐
111 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
112 setLoading(true)
113 setError(null)
114 try {
22301008ba662fe2025-06-20 18:10:20 +0800115 const currentUserId = getCurrentUserId()
116 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
22301008b86c21c2025-06-20 19:17:00 +0800117 // 新增:拉取详情,保证和原始数据一致
118 const detailed = await Promise.all(
119 (data.recommendations || []).map(async item => {
120 try {
121 const d = await fetchPost(item.id)
122 return {
123 id: d.id,
124 title: d.title,
125 author: `作者 ${d.user_id}`,
126 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
127 img: d.media_urls?.[0] || '',
128 likes: d.heat,
129 content: d.content || ''
130 }
131 } catch {
132 // 拉详情失败时兜底
133 return {
134 id: item.id,
135 title: item.title,
136 author: item.author || '佚名',
137 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
138 img: item.img || '',
139 likes: item.heat || 0,
140 content: item.content || ''
141 }
142 }
143 })
144 )
145 setItems(detailed)
95630366980c1f272025-06-20 14:08:54 +0800146 } catch (e) {
147 console.error('协同过滤推荐失败:', e)
148 setError('协同过滤推荐失败')
149 setItems([])
150 }
151 setLoading(false)
152 }, [recCFNum])
22301008b86c21c2025-06-20 19:17:00 +0800153 // 深度推荐
154 const fetchDeepRecommend = useCallback(async (topN = 20) => {
155 setLoading(true)
156 setError(null)
157 try {
158 const currentUserId = getCurrentUserId()
159 const recs = await deepRecommend(currentUserId, topN)
160 // 拉取详情,保证和原始数据一致
161 const detailed = await Promise.all(
162 (recs || []).map(async item => {
163 try {
164 const d = await fetchPost(item.id)
165 return {
166 id: d.id,
167 title: d.title,
168 author: `作者 ${d.user_id}`,
169 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
170 img: d.media_urls?.[0] || '',
171 likes: d.heat,
172 content: d.content || ''
173 }
174 } catch {
175 return {
176 id: item.id,
177 title: item.title,
178 author: item.author || '佚名',
179 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
180 img: item.img || '',
181 likes: item.heat || 0,
182 content: item.content || ''
183 }
184 }
185 })
186 )
187 setItems(detailed)
188 } catch (e) {
189 setError('深度推荐失败')
190 setItems([])
191 }
192 setLoading(false)
193 }, [])
95630366980c1f272025-06-20 14:08:54 +0800194 // 获取用户兴趣标签后再推荐
195 const fetchUserTagsAndRecommend = useCallback(async () => {
196 setLoading(true)
197 setError(null)
198 let tags = []
199 try {
22301008ba662fe2025-06-20 18:10:20 +0800200 const currentUserId = getCurrentUserId()
201 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800202 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
203 } catch {
204 tags = DEFAULT_TAGS
205 }
206 if (recMode === 'tag') {
207 await fetchTagRecommend(tags)
22301008b86c21c2025-06-20 19:17:00 +0800208 } else if (recMode === 'cf') {
95630366980c1f272025-06-20 14:08:54 +0800209 await fetchCFRecommend()
22301008b86c21c2025-06-20 19:17:00 +0800210 } else if (recMode === 'deep') {
211 await fetchDeepRecommend()
95630366980c1f272025-06-20 14:08:54 +0800212 }
213 setLoading(false)
22301008b86c21c2025-06-20 19:17:00 +0800214 }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800215
216 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800217 // 原始数据加载函数
218 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800219 try {
220 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
221 // 为了拿到 media_urls 和 user_id,这里再拉详情
222 const detailed = await Promise.all(
223 list.map(async p => {
224 const d = await fetchPost(p.id)
225 return {
226 id: d.id,
227 title: d.title,
228 author: `作者 ${d.user_id}`,
229 avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
230 img: d.media_urls?.[0] || '', // 用第一张媒体作为封面
231 likes: d.heat
232 }
233 })
234 )
235 setItems(detailed)
236 } catch (e) {
237 setError(e.message)
238 } finally {
239 setLoading(false)
240 }
241 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800242
95630366980c1f272025-06-20 14:08:54 +0800243 // 根据模式选择加载方式
244 if (activeCat === '推荐' && useSearchRecommend) {
245 fetchUserTagsAndRecommend()
246 } else {
247 loadPosts()
248 }
249 }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
250 // 切换推荐模式时的额外处理
251 useEffect(() => {
252 if (activeCat === '推荐' && useSearchRecommend) {
253 fetchUserTagsAndRecommend()
254 }
255 // eslint-disable-next-line
256 }, [recMode, fetchUserTagsAndRecommend])
257
258 // 根据模式选择不同的加载方式
259 const handleSearch = e => {
260 e.preventDefault()
261 if (useSearchRecommend) {
262 fetchSearchContent(search)
263 } else {
264 // 切换到搜索推荐模式
265 setUseSearchRecommend(true)
266 fetchSearchContent(search)
267 }
268 }
269
270 const handlePostClick = (postId) => {
271 navigate(`/post/${postId}`)
272 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800273 return (
274 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800275 {/* 数据源切换 */}
276 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
277 <span>数据源:</span>
278 <div style={{display:'flex', gap:8}}>
279 <button
280 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
281 onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
282 type="button"
283 style={{
284 borderRadius: 20,
285 padding: '4px 18px',
286 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
287 background: !useSearchRecommend ? '#fff0f0' : '#fff',
288 color: !useSearchRecommend ? '#e84c4a' : '#333',
289 fontWeight: !useSearchRecommend ? 600 : 400,
290 cursor: 'pointer',
291 transition: 'all 0.2s',
292 outline: 'none',
293 }}
294 >原始数据</button>
295 <button
296 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
297 onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
298 type="button"
299 style={{
300 borderRadius: 20,
301 padding: '4px 18px',
302 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
303 background: useSearchRecommend ? '#fff0f0' : '#fff',
304 color: useSearchRecommend ? '#e84c4a' : '#333',
305 fontWeight: useSearchRecommend ? 600 : 400,
306 cursor: 'pointer',
307 transition: 'all 0.2s',
308 outline: 'none',
309 }}
310 >智能推荐</button>
311 </div>
312 </div>
313
314 {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
315 {activeCat === '推荐' && useSearchRecommend && (
316 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
317 <span style={{marginRight:8}}>推荐模式:</span>
318 <div style={{display:'flex', gap:8}}>
319 {recommendModes.map(m => (
320 <button
321 key={m.value}
322 className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
323 onClick={() => setRecMode(m.value)}
324 type="button"
325 style={{
326 borderRadius: 20,
327 padding: '4px 18px',
328 border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
329 background: recMode===m.value ? '#fff0f0' : '#fff',
330 color: recMode===m.value ? '#e84c4a' : '#333',
331 fontWeight: recMode===m.value ? 600 : 400,
332 cursor: 'pointer',
333 transition: 'all 0.2s',
334 outline: 'none',
335 }}
336 >{m.label}</button>
337 ))}
338 </div>
339 {/* 协同过滤推荐数量选择 */}
340 {recMode === 'cf' && (
341 <div style={{display:'flex',alignItems:'center',gap:4}}>
342 <span>推荐数量:</span>
343 <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'}}>
344 {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
345 </select>
346 </div>
347 )}
348 </div>
349 )}
350
351 {/* 搜索栏 */}
352 <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
353 <input
354 type="text"
355 className="search-input"
356 placeholder="搜索内容/标题/标签"
357 value={search}
358 onChange={e => setSearch(e.target.value)}
359 />
360 <button type="submit" className="search-btn">搜索</button>
361 </form>
362
TRM-codingd1cbf672025-06-18 15:15:08 +0800363 {/* 顶部分类 */}
364 <nav className="feed-tabs">
365 {categories.map(cat => (
366 <button
367 key={cat}
368 className={cat === activeCat ? 'tab active' : 'tab'}
95630366980c1f272025-06-20 14:08:54 +0800369 onClick={() => {
370 setActiveCat(cat);
371 setSearch('');
372 if (useSearchRecommend) {
373 if (cat === '推荐') {
374 fetchUserTagsAndRecommend()
375 } else {
376 fetchSearchContent()
377 }
378 }
379 }}
TRM-codingd1cbf672025-06-18 15:15:08 +0800380 >
381 {cat}
382 </button>
383 ))}
95630366980c1f272025-06-20 14:08:54 +0800384 </nav> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800385 {loading ? (
386 <div className="loading">加载中…</div>
387 ) : error ? (
388 <div className="error">加载失败:{error}</div>
389 ) : (
390 /* 瀑布流卡片区 */
391 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800392 {items.length === 0 ? (
393 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
394 ) : (
395 items.map(item => (
396 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
397 {item.img && <img className="card-img" src={item.img} alt={item.title} />}
398 <h3 className="card-title">{item.title}</h3>
399 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
400 <div className="card-footer">
401 <div className="card-author">
402 <img className="avatar" src={item.avatar} alt={item.author} />
403 <span className="username">{item.author}</span>
404 </div>
405 <div className="card-likes">
406 <ThumbsUp size={16} />
407 <span className="likes-count">{item.likes}</span>
408 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800409 </div>
410 </div>
95630366980c1f272025-06-20 14:08:54 +0800411 ))
412 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800413 </div>
414 )}
415 </div>
416 )
417}