| // src/components/HomeFeed.jsx |
| |
| import React, { useState, useEffect, useCallback } from 'react' |
| import { useNavigate } from 'react-router-dom' |
| import { ThumbsUp } from 'lucide-react' |
| import { fetchPosts, fetchPost } from '../api/posts_wzy' |
| import { searchAPI } from '../api/search_jwlll' |
| import { getUserInfo } from '../utils/auth' |
| import { deepRecommend } from '../api/recommend_rhj' |
| import postsAPI from '../api/posts_api' |
| import '../style/HomeFeed.css' |
| |
| const categories = [ |
| '推荐','穿搭','美食','彩妆','影视', |
| '职场','情感','家居','游戏','旅行','健身' |
| ] |
| |
| const recommendModes = [ |
| { label: '标签推荐', value: 'tag' }, |
| { label: '协同过滤推荐', value: 'cf' }, |
| { label: '深度推荐', value: 'deep' } // 新增 |
| ] |
| |
| const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值) |
| const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整 |
| |
| export default function HomeFeed() { |
| const navigate = useNavigate() |
| |
| // 获取当前用户ID,如果未登录则使用默认值 |
| const getCurrentUserId = () => { |
| const userInfo = getUserInfo() |
| return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID |
| } |
| const [activeCat, setActiveCat] = useState('推荐') |
| const [items, setItems] = useState([]) |
| const [loading, setLoading] = useState(true) |
| const [error, setError] = useState(null) |
| // JWLLL 搜索推荐相关状态 |
| const [search, setSearch] = useState('') |
| const [recMode, setRecMode] = useState('tag') |
| const [recCFNum, setRecCFNum] = useState(20) |
| const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数 |
| const [userMap, setUserMap] = useState({}) // user_id: {username, nickname} |
| |
| // JWLLL搜索推荐内容 |
| const fetchSearchContent = useCallback(async (keyword = '') => { |
| setLoading(true) |
| setError(null) |
| try { |
| const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat) |
| // 新增:拉取详情,保证和推荐一致 |
| const detailed = await Promise.all( |
| (data.results || []).map(async item => { |
| try { |
| const d = await fetchPost(item.id) |
| return { |
| id: d.id, |
| title: d.title, |
| author: `作者 ${d.user_id}`, |
| avatar: `https://i.pravatar.cc/40?img=${d.user_id}`, |
| img: d.media_urls?.[0] || '', |
| likes: d.heat, |
| content: d.content || '' |
| } |
| } catch { |
| return { |
| id: item.id, |
| title: item.title, |
| author: item.author || '佚名', |
| avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| img: item.img || '', |
| likes: item.heat || 0, |
| content: item.content || '' |
| } |
| } |
| }) |
| ) |
| setItems(detailed) |
| } catch (e) { |
| console.error('搜索失败:', e) |
| setError('搜索失败') |
| setItems([]) |
| } |
| setLoading(false) |
| }, [activeCat]) |
| // 标签推荐 |
| const fetchTagRecommend = useCallback(async (tags) => { |
| setLoading(true) |
| setError(null) |
| try { |
| const currentUserId = getCurrentUserId() |
| const data = await searchAPI.recommendByTags(currentUserId, tags) |
| // 新增:拉取详情,保证和原始数据一致 |
| const detailed = await Promise.all( |
| (data.recommendations || []).map(async item => { |
| try { |
| const d = await fetchPost(item.id) |
| return { |
| id: d.id, |
| title: d.title, |
| author: `作者 ${d.user_id}`, |
| avatar: `https://i.pravatar.cc/40?img=${d.user_id}`, |
| img: d.media_urls?.[0] || '', |
| likes: d.heat, |
| content: d.content || '' |
| } |
| } catch { |
| // 拉详情失败时兜底 |
| return { |
| id: item.id, |
| title: item.title, |
| author: item.author || '佚名', |
| avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| img: item.img || '', |
| likes: item.heat || 0, |
| content: item.content || '' |
| } |
| } |
| }) |
| ) |
| setItems(detailed) |
| } catch (e) { |
| console.error('标签推荐失败:', e) |
| setError('标签推荐失败') |
| setItems([]) |
| } |
| setLoading(false) |
| }, []) |
| // 协同过滤推荐 |
| const fetchCFRecommend = useCallback(async (topN = recCFNum) => { |
| setLoading(true) |
| setError(null) |
| try { |
| const currentUserId = getCurrentUserId() |
| const data = await searchAPI.userBasedRecommend(currentUserId, topN) |
| // 新增:拉取详情,保证和原始数据一致 |
| const detailed = await Promise.all( |
| (data.recommendations || []).map(async item => { |
| try { |
| const d = await fetchPost(item.id) |
| return { |
| id: d.id, |
| title: d.title, |
| author: `作者 ${d.user_id}`, |
| avatar: `https://i.pravatar.cc/40?img=${d.user_id}`, |
| img: d.media_urls?.[0] || '', |
| likes: d.heat, |
| content: d.content || '' |
| } |
| } catch { |
| // 拉详情失败时兜底 |
| return { |
| id: item.id, |
| title: item.title, |
| author: item.author || '佚名', |
| avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| img: item.img || '', |
| likes: item.heat || 0, |
| content: item.content || '' |
| } |
| } |
| }) |
| ) |
| setItems(detailed) |
| } catch (e) { |
| console.error('协同过滤推荐失败:', e) |
| setError('协同过滤推荐失败') |
| setItems([]) |
| } |
| setLoading(false) |
| }, [recCFNum]) |
| // 深度推荐 |
| const fetchDeepRecommend = useCallback(async (topN = 20) => { |
| setLoading(true) |
| setError(null) |
| try { |
| const currentUserId = getCurrentUserId() |
| const recs = await deepRecommend(currentUserId, topN) |
| // 拉取详情,保证和原始数据一致 |
| const detailed = await Promise.all( |
| (recs || []).map(async item => { |
| try { |
| const d = await fetchPost(item.id) |
| return { |
| id: d.id, |
| title: d.title, |
| author: `作者 ${d.user_id}`, |
| avatar: `https://i.pravatar.cc/40?img=${d.user_id}`, |
| img: d.media_urls?.[0] || '', |
| likes: d.heat, |
| content: d.content || '' |
| } |
| } catch { |
| return { |
| id: item.id, |
| title: item.title, |
| author: item.author || '佚名', |
| avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| img: item.img || '', |
| likes: item.heat || 0, |
| content: item.content || '' |
| } |
| } |
| }) |
| ) |
| setItems(detailed) |
| } catch (e) { |
| setError('深度推荐失败') |
| setItems([]) |
| } |
| setLoading(false) |
| }, []) |
| // 获取用户兴趣标签后再推荐 |
| const fetchUserTagsAndRecommend = useCallback(async () => { |
| setLoading(true) |
| setError(null) |
| let tags = [] |
| try { |
| const currentUserId = getCurrentUserId() |
| const data = await searchAPI.getUserTags(currentUserId) |
| tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS |
| } catch { |
| tags = DEFAULT_TAGS |
| } |
| if (recMode === 'tag') { |
| await fetchTagRecommend(tags) |
| } else if (recMode === 'cf') { |
| await fetchCFRecommend() |
| } else if (recMode === 'deep') { |
| await fetchDeepRecommend() |
| } |
| setLoading(false) |
| }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend]) |
| |
| // 拉取所有涉及用户的昵称 |
| const fetchUserNames = async (userIds) => { |
| const map = {} |
| await Promise.all(userIds.map(async uid => { |
| try { |
| const user = await postsAPI.getUser(uid) |
| map[uid] = user.username || user.nickname || `用户${uid}` |
| } catch { |
| map[uid] = `用户${uid}` |
| } |
| })) |
| setUserMap(map) |
| } |
| |
| useEffect(() => { |
| // 原始数据加载函数 |
| const loadPosts = async () => { |
| try { |
| const list = await fetchPosts() // [{id, title, heat, created_at}, …] |
| // 为了拿到 media_urls 和 user_id,这里再拉详情 |
| const detailed = await Promise.all( |
| list.map(async p => { |
| const d = await fetchPost(p.id) |
| return { |
| id: d.id, |
| title: d.title, |
| author: `作者 ${d.user_id}`, |
| // avatar: `https://i.pravatar.cc/40?img=${d.user_id}`, |
| img: d.media_urls?.[0] || '', // 用第一张媒体作为封面 |
| likes: d.heat |
| } |
| }) |
| ) |
| setItems(detailed) |
| // 拉取所有涉及用户的昵称 |
| const userIds = [...new Set(detailed.map(i => i.authorId))] |
| fetchUserNames(userIds) |
| } catch (e) { |
| setError(e.message) |
| } finally { |
| setLoading(false) |
| } |
| } |
| |
| // 根据模式选择加载方式 |
| if (activeCat === '推荐' && useSearchRecommend) { |
| fetchUserTagsAndRecommend() |
| } else { |
| loadPosts() |
| } |
| }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend]) |
| // 切换推荐模式时的额外处理 |
| useEffect(() => { |
| if (activeCat === '推荐' && useSearchRecommend) { |
| fetchUserTagsAndRecommend() |
| } |
| // eslint-disable-next-line |
| }, [recMode, fetchUserTagsAndRecommend]) |
| |
| // 根据模式选择不同的加载方式 |
| const handleSearch = e => { |
| e.preventDefault() |
| if (useSearchRecommend) { |
| fetchSearchContent(search) |
| } else { |
| // 切换到搜索推荐模式 |
| setUseSearchRecommend(true) |
| fetchSearchContent(search) |
| } |
| } |
| |
| const handlePostClick = (postId) => { |
| navigate(`/post/${postId}`) |
| } |
| return ( |
| <div className="home-feed"> |
| {/* 数据源切换 */} |
| <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}> |
| <span>数据源:</span> |
| <div style={{display:'flex', gap:8}}> |
| <button |
| className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'} |
| onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}} |
| type="button" |
| style={{ |
| borderRadius: 20, |
| padding: '4px 18px', |
| border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc', |
| background: !useSearchRecommend ? '#fff0f0' : '#fff', |
| color: !useSearchRecommend ? '#e84c4a' : '#333', |
| fontWeight: !useSearchRecommend ? 600 : 400, |
| cursor: 'pointer', |
| transition: 'all 0.2s', |
| outline: 'none', |
| }} |
| >原始数据</button> |
| <button |
| className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'} |
| onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}} |
| type="button" |
| style={{ |
| borderRadius: 20, |
| padding: '4px 18px', |
| border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc', |
| background: useSearchRecommend ? '#fff0f0' : '#fff', |
| color: useSearchRecommend ? '#e84c4a' : '#333', |
| fontWeight: useSearchRecommend ? 600 : 400, |
| cursor: 'pointer', |
| transition: 'all 0.2s', |
| outline: 'none', |
| }} |
| >智能推荐</button> |
| </div> |
| </div> |
| |
| {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */} |
| {activeCat === '推荐' && useSearchRecommend && ( |
| <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}> |
| <span style={{marginRight:8}}>推荐模式:</span> |
| <div style={{display:'flex', gap:8}}> |
| {recommendModes.map(m => ( |
| <button |
| key={m.value} |
| className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'} |
| onClick={() => setRecMode(m.value)} |
| type="button" |
| style={{ |
| borderRadius: 20, |
| padding: '4px 18px', |
| border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc', |
| background: recMode===m.value ? '#fff0f0' : '#fff', |
| color: recMode===m.value ? '#e84c4a' : '#333', |
| fontWeight: recMode===m.value ? 600 : 400, |
| cursor: 'pointer', |
| transition: 'all 0.2s', |
| outline: 'none', |
| }} |
| >{m.label}</button> |
| ))} |
| </div> |
| {/* 协同过滤推荐数量选择 */} |
| {recMode === 'cf' && ( |
| <div style={{display:'flex',alignItems:'center',gap:4}}> |
| <span>推荐数量:</span> |
| <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'}}> |
| {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)} |
| </select> |
| </div> |
| )} |
| </div> |
| )} |
| |
| {/* 搜索栏 */} |
| <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}> |
| <input |
| type="text" |
| className="search-input" |
| placeholder="搜索内容/标题/标签" |
| value={search} |
| onChange={e => setSearch(e.target.value)} |
| /> |
| <button type="submit" className="search-btn">搜索</button> |
| </form> |
| |
| {/* 顶部分类 */} |
| <nav className="feed-tabs"> |
| {categories.map(cat => ( |
| <button |
| key={cat} |
| className={cat === activeCat ? 'tab active' : 'tab'} |
| onClick={() => { |
| setActiveCat(cat); |
| setSearch(''); |
| if (useSearchRecommend) { |
| if (cat === '推荐') { |
| fetchUserTagsAndRecommend() |
| } else { |
| fetchSearchContent() |
| } |
| } |
| }} |
| > |
| {cat} |
| </button> |
| ))} |
| </nav> {/* 状态提示 */} |
| {loading ? ( |
| <div className="loading">加载中…</div> |
| ) : error ? ( |
| <div className="error">加载失败:{error}</div> |
| ) : ( |
| /* 瀑布流卡片区 */ |
| <div className="feed-grid"> |
| {items.length === 0 ? ( |
| <div style={{padding:32, color:'#aaa'}}>暂无内容</div> |
| ) : ( |
| items.map(item => ( |
| <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}> |
| {item.img && <img className="card-img" src={item.img} alt={item.title} />} |
| <h3 className="card-title">{item.title}</h3> |
| {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>} |
| <div className="card-footer"> |
| <div className="card-author"> |
| <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId} /> |
| <span className="username">{userMap[item.authorId] || item.authorId}</span> |
| </div> |
| <div className="card-likes"> |
| <ThumbsUp size={16} /> |
| <span className="likes-count">{item.likes}</span> |
| </div> |
| </div> |
| </div> |
| )) |
| )} |
| </div> |
| )} |
| </div> |
| ) |
| } |