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