blob: cc37642979986c05942fa7a0d27a3c9c27de19a7 [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'
TRM-codingd1cbf672025-06-18 15:15:08 +08009import '../style/HomeFeed.css'
10
11const categories = [
12 '推荐','穿搭','美食','彩妆','影视',
13 '职场','情感','家居','游戏','旅行','健身'
14]
15
95630366980c1f272025-06-20 14:08:54 +080016const recommendModes = [
17 { label: '标签推荐', value: 'tag' },
18 { label: '协同过滤推荐', value: 'cf' }
19]
20
22301008ba662fe2025-06-20 18:10:20 +080021const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值)
95630366980c1f272025-06-20 14:08:54 +080022const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
23
TRM-codingd1cbf672025-06-18 15:15:08 +080024export default function HomeFeed() {
TRM-coding29174c22025-06-18 23:56:51 +080025 const navigate = useNavigate()
22301008ba662fe2025-06-20 18:10:20 +080026
27 // 获取当前用户ID,如果未登录则使用默认值
28 const getCurrentUserId = () => {
29 const userInfo = getUserInfo()
30 return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID
31 }
TRM-codingd1cbf672025-06-18 15:15:08 +080032 const [activeCat, setActiveCat] = useState('推荐')
33 const [items, setItems] = useState([])
34 const [loading, setLoading] = useState(true)
35 const [error, setError] = useState(null)
95630366980c1f272025-06-20 14:08:54 +080036 // JWLLL 搜索推荐相关状态
37 const [search, setSearch] = useState('')
38 const [recMode, setRecMode] = useState('tag')
39 const [recCFNum, setRecCFNum] = useState(20)
40 const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
41
42 // JWLLL搜索推荐内容
43 const fetchSearchContent = useCallback(async (keyword = '') => {
44 setLoading(true)
45 setError(null)
46 try {
47 const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
48 const formattedItems = (data.results || []).map(item => ({
49 id: item.id,
50 title: item.title,
51 author: item.author || '佚名',
52 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
53 img: item.img || '',
54 likes: item.heat || 0,
55 content: item.content
56 }))
57 setItems(formattedItems)
58 } catch (e) {
59 console.error('搜索失败:', e)
60 setError('搜索失败')
61 setItems([])
62 }
63 setLoading(false)
64 }, [activeCat])
95630366980c1f272025-06-20 14:08:54 +080065 // 标签推荐
66 const fetchTagRecommend = useCallback(async (tags) => {
67 setLoading(true)
68 setError(null)
69 try {
22301008ba662fe2025-06-20 18:10:20 +080070 const currentUserId = getCurrentUserId()
71 const data = await searchAPI.recommendByTags(currentUserId, tags)
95630366980c1f272025-06-20 14:08:54 +080072 const formattedItems = (data.recommendations || []).map(item => ({
73 id: item.id,
74 title: item.title,
75 author: item.author || '佚名',
76 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
77 img: item.img || '',
78 likes: item.heat || 0,
79 content: item.content
80 }))
81 setItems(formattedItems)
82 } catch (e) {
83 console.error('标签推荐失败:', e)
84 setError('标签推荐失败')
85 setItems([])
86 }
87 setLoading(false)
88 }, [])
95630366980c1f272025-06-20 14:08:54 +080089 // 协同过滤推荐
90 const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
91 setLoading(true)
92 setError(null)
93 try {
22301008ba662fe2025-06-20 18:10:20 +080094 const currentUserId = getCurrentUserId()
95 const data = await searchAPI.userBasedRecommend(currentUserId, topN)
95630366980c1f272025-06-20 14:08:54 +080096 const formattedItems = (data.recommendations || []).map(item => ({
97 id: item.id,
98 title: item.title,
99 author: item.author || '佚名',
100 avatar: `https://i.pravatar.cc/40?img=${item.id}`,
101 img: item.img || '',
102 likes: item.heat || 0,
103 content: item.content
104 }))
105 setItems(formattedItems)
106 } catch (e) {
107 console.error('协同过滤推荐失败:', e)
108 setError('协同过滤推荐失败')
109 setItems([])
110 }
111 setLoading(false)
112 }, [recCFNum])
95630366980c1f272025-06-20 14:08:54 +0800113 // 获取用户兴趣标签后再推荐
114 const fetchUserTagsAndRecommend = useCallback(async () => {
115 setLoading(true)
116 setError(null)
117 let tags = []
118 try {
22301008ba662fe2025-06-20 18:10:20 +0800119 const currentUserId = getCurrentUserId()
120 const data = await searchAPI.getUserTags(currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800121 tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
122 } catch {
123 tags = DEFAULT_TAGS
124 }
125 if (recMode === 'tag') {
126 await fetchTagRecommend(tags)
127 } else {
128 await fetchCFRecommend()
129 }
130 setLoading(false)
131 }, [recMode, fetchTagRecommend, fetchCFRecommend])
TRM-codingd1cbf672025-06-18 15:15:08 +0800132
133 useEffect(() => {
95630366980c1f272025-06-20 14:08:54 +0800134 // 原始数据加载函数
135 const loadPosts = async () => {
TRM-codingd1cbf672025-06-18 15:15:08 +0800136 try {
137 const list = await fetchPosts() // [{id, title, heat, created_at}, …]
138 // 为了拿到 media_urls 和 user_id,这里再拉详情
139 const detailed = await Promise.all(
140 list.map(async p => {
141 const d = await fetchPost(p.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 }
150 })
151 )
152 setItems(detailed)
153 } catch (e) {
154 setError(e.message)
155 } finally {
156 setLoading(false)
157 }
158 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800159
95630366980c1f272025-06-20 14:08:54 +0800160 // 根据模式选择加载方式
161 if (activeCat === '推荐' && useSearchRecommend) {
162 fetchUserTagsAndRecommend()
163 } else {
164 loadPosts()
165 }
166 }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
167 // 切换推荐模式时的额外处理
168 useEffect(() => {
169 if (activeCat === '推荐' && useSearchRecommend) {
170 fetchUserTagsAndRecommend()
171 }
172 // eslint-disable-next-line
173 }, [recMode, fetchUserTagsAndRecommend])
174
175 // 根据模式选择不同的加载方式
176 const handleSearch = e => {
177 e.preventDefault()
178 if (useSearchRecommend) {
179 fetchSearchContent(search)
180 } else {
181 // 切换到搜索推荐模式
182 setUseSearchRecommend(true)
183 fetchSearchContent(search)
184 }
185 }
186
187 const handlePostClick = (postId) => {
188 navigate(`/post/${postId}`)
189 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800190 return (
191 <div className="home-feed">
95630366980c1f272025-06-20 14:08:54 +0800192 {/* 数据源切换 */}
193 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
194 <span>数据源:</span>
195 <div style={{display:'flex', gap:8}}>
196 <button
197 className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
198 onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
199 type="button"
200 style={{
201 borderRadius: 20,
202 padding: '4px 18px',
203 border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
204 background: !useSearchRecommend ? '#fff0f0' : '#fff',
205 color: !useSearchRecommend ? '#e84c4a' : '#333',
206 fontWeight: !useSearchRecommend ? 600 : 400,
207 cursor: 'pointer',
208 transition: 'all 0.2s',
209 outline: 'none',
210 }}
211 >原始数据</button>
212 <button
213 className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
214 onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
215 type="button"
216 style={{
217 borderRadius: 20,
218 padding: '4px 18px',
219 border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
220 background: useSearchRecommend ? '#fff0f0' : '#fff',
221 color: useSearchRecommend ? '#e84c4a' : '#333',
222 fontWeight: useSearchRecommend ? 600 : 400,
223 cursor: 'pointer',
224 transition: 'all 0.2s',
225 outline: 'none',
226 }}
227 >智能推荐</button>
228 </div>
229 </div>
230
231 {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
232 {activeCat === '推荐' && useSearchRecommend && (
233 <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
234 <span style={{marginRight:8}}>推荐模式:</span>
235 <div style={{display:'flex', gap:8}}>
236 {recommendModes.map(m => (
237 <button
238 key={m.value}
239 className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
240 onClick={() => setRecMode(m.value)}
241 type="button"
242 style={{
243 borderRadius: 20,
244 padding: '4px 18px',
245 border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
246 background: recMode===m.value ? '#fff0f0' : '#fff',
247 color: recMode===m.value ? '#e84c4a' : '#333',
248 fontWeight: recMode===m.value ? 600 : 400,
249 cursor: 'pointer',
250 transition: 'all 0.2s',
251 outline: 'none',
252 }}
253 >{m.label}</button>
254 ))}
255 </div>
256 {/* 协同过滤推荐数量选择 */}
257 {recMode === 'cf' && (
258 <div style={{display:'flex',alignItems:'center',gap:4}}>
259 <span>推荐数量:</span>
260 <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'}}>
261 {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
262 </select>
263 </div>
264 )}
265 </div>
266 )}
267
268 {/* 搜索栏 */}
269 <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
270 <input
271 type="text"
272 className="search-input"
273 placeholder="搜索内容/标题/标签"
274 value={search}
275 onChange={e => setSearch(e.target.value)}
276 />
277 <button type="submit" className="search-btn">搜索</button>
278 </form>
279
TRM-codingd1cbf672025-06-18 15:15:08 +0800280 {/* 顶部分类 */}
281 <nav className="feed-tabs">
282 {categories.map(cat => (
283 <button
284 key={cat}
285 className={cat === activeCat ? 'tab active' : 'tab'}
95630366980c1f272025-06-20 14:08:54 +0800286 onClick={() => {
287 setActiveCat(cat);
288 setSearch('');
289 if (useSearchRecommend) {
290 if (cat === '推荐') {
291 fetchUserTagsAndRecommend()
292 } else {
293 fetchSearchContent()
294 }
295 }
296 }}
TRM-codingd1cbf672025-06-18 15:15:08 +0800297 >
298 {cat}
299 </button>
300 ))}
95630366980c1f272025-06-20 14:08:54 +0800301 </nav> {/* 状态提示 */}
TRM-codingd1cbf672025-06-18 15:15:08 +0800302 {loading ? (
303 <div className="loading">加载中…</div>
304 ) : error ? (
305 <div className="error">加载失败:{error}</div>
306 ) : (
307 /* 瀑布流卡片区 */
308 <div className="feed-grid">
95630366980c1f272025-06-20 14:08:54 +0800309 {items.length === 0 ? (
310 <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
311 ) : (
312 items.map(item => (
313 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
314 {item.img && <img className="card-img" src={item.img} alt={item.title} />}
315 <h3 className="card-title">{item.title}</h3>
316 {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
317 <div className="card-footer">
318 <div className="card-author">
319 <img className="avatar" src={item.avatar} alt={item.author} />
320 <span className="username">{item.author}</span>
321 </div>
322 <div className="card-likes">
323 <ThumbsUp size={16} />
324 <span className="likes-count">{item.likes}</span>
325 </div>
TRM-codingd1cbf672025-06-18 15:15:08 +0800326 </div>
327 </div>
95630366980c1f272025-06-20 14:08:54 +0800328 ))
329 )}
TRM-codingd1cbf672025-06-18 15:15:08 +0800330 </div>
331 )}
332 </div>
333 )
334}