22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 1 | import React, { useState, useEffect } from 'react' |
| 2 | import { ThumbsUp } from 'lucide-react' |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 3 | import { useNavigate } from 'react-router-dom' |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 4 | import '../style/HomeFeed.css' |
| 5 | |
| 6 | const categories = [ |
| 7 | '推荐','穿搭','美食','彩妆','影视', |
| 8 | '职场','情感','家居','游戏','旅行','健身' |
| 9 | ] |
| 10 | |
| 11 | const recommendModes = [ |
| 12 | { label: '标签推荐', value: 'tag' }, |
| 13 | { label: '协同过滤推荐', value: 'cf' } |
| 14 | ] |
| 15 | |
| 16 | const DEFAULT_USER_ID = '3' // 确保数据库有此用户 |
| 17 | const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整 |
| 18 | |
| 19 | export default function HomeFeed() { |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 20 | const navigate = useNavigate() |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 21 | const [activeCat, setActiveCat] = useState('推荐') |
| 22 | const [items, setItems] = useState([]) |
| 23 | const [search, setSearch] = useState('') |
| 24 | const [loading, setLoading] = useState(false) |
| 25 | const [recMode, setRecMode] = useState('tag') |
| 26 | const [userTags, setUserTags] = useState([]) |
| 27 | const [recCFNum, setRecCFNum] = useState(20) |
| 28 | |
| 29 | // 首次进入首页自动推荐内容 |
| 30 | useEffect(() => { |
| 31 | if (activeCat === '推荐') { |
| 32 | fetchUserTagsAndRecommend() |
| 33 | } else { |
| 34 | fetchContent() |
| 35 | } |
| 36 | // eslint-disable-next-line |
| 37 | }, []) |
| 38 | |
| 39 | // 切换推荐模式或分类时刷新内容 |
| 40 | useEffect(() => { |
| 41 | if (activeCat === '推荐') { |
| 42 | fetchUserTagsAndRecommend() |
| 43 | } else { |
| 44 | fetchContent() |
| 45 | } |
| 46 | // eslint-disable-next-line |
| 47 | }, [activeCat, recMode]) |
| 48 | |
| 49 | // 获取用户兴趣标签后再推荐 |
| 50 | const fetchUserTagsAndRecommend = async () => { |
| 51 | setLoading(true) |
| 52 | let tags = [] |
| 53 | try { |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 54 | const res = await fetch(`http://127.0.0.1:5000/user_tags?user_id=${DEFAULT_USER_ID}`) |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 55 | const data = await res.json() |
| 56 | tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS |
| 57 | setUserTags(tags) |
| 58 | } catch { |
| 59 | tags = DEFAULT_TAGS |
| 60 | setUserTags(tags) |
| 61 | } |
| 62 | if (recMode === 'tag') { |
| 63 | await fetchTagRecommend(tags) |
| 64 | } else { |
| 65 | await fetchCFRecommend() |
| 66 | } |
| 67 | setLoading(false) |
| 68 | } |
| 69 | |
| 70 | const fetchContent = async (keyword = '') => { |
| 71 | setLoading(true) |
| 72 | try { |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 73 | const res = await fetch('http://127.0.0.1:5000/search', { |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 74 | method: 'POST', |
| 75 | headers: { 'Content-Type': 'application/json' }, |
| 76 | body: JSON.stringify({ keyword: keyword || activeCat, category: activeCat === '推荐' ? undefined : activeCat }) |
| 77 | }) |
| 78 | const data = await res.json() |
| 79 | setItems(data.results || []) |
| 80 | } catch (e) { |
| 81 | setItems([]) |
| 82 | } |
| 83 | setLoading(false) |
| 84 | } |
| 85 | |
| 86 | // 标签推荐 |
| 87 | const fetchTagRecommend = async (tags) => { |
| 88 | setLoading(true) |
| 89 | try { |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 90 | const res = await fetch('http://127.0.0.1:5000/recommend_tags', { |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 91 | method: 'POST', |
| 92 | headers: { 'Content-Type': 'application/json' }, |
| 93 | body: JSON.stringify({ user_id: DEFAULT_USER_ID, tags }) |
| 94 | }) |
| 95 | const data = await res.json() |
| 96 | setItems(data.recommendations || []) |
| 97 | } catch (e) { |
| 98 | setItems([]) |
| 99 | } |
| 100 | setLoading(false) |
| 101 | } |
| 102 | |
| 103 | // 协同过滤推荐 |
| 104 | const fetchCFRecommend = async (topN = recCFNum) => { |
| 105 | setLoading(true) |
| 106 | try { |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 107 | const res = await fetch('http://127.0.0.1:5000/user_based_recommend', { |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 108 | method: 'POST', |
| 109 | headers: { 'Content-Type': 'application/json' }, |
| 110 | body: JSON.stringify({ user_id: DEFAULT_USER_ID, top_n: topN }) |
| 111 | }) |
| 112 | const data = await res.json() |
| 113 | setItems(data.recommendations || []) |
| 114 | } catch (e) { |
| 115 | setItems([]) |
| 116 | } |
| 117 | setLoading(false) |
| 118 | } |
| 119 | |
| 120 | const handleSearch = e => { |
| 121 | e.preventDefault() |
| 122 | fetchContent(search) |
| 123 | } |
| 124 | |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 125 | const handlePostClick = (postId) => { |
| 126 | navigate(`/post/${postId}`) |
| 127 | } |
| 128 | |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 129 | return ( |
| 130 | <div className="home-feed"> |
| 131 | {/* 推荐模式切换,仅在推荐页显示 */} |
| 132 | {activeCat === '推荐' && ( |
| 133 | <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}> |
| 134 | <span style={{marginRight:8}}>推荐模式:</span> |
| 135 | <div style={{display:'flex', gap:8}}> |
| 136 | {recommendModes.map(m => ( |
| 137 | <button |
| 138 | key={m.value} |
| 139 | className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'} |
| 140 | onClick={() => setRecMode(m.value)} |
| 141 | type="button" |
| 142 | style={{ |
| 143 | borderRadius: 20, |
| 144 | padding: '4px 18px', |
| 145 | border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc', |
| 146 | background: recMode===m.value ? '#fff0f0' : '#fff', |
| 147 | color: recMode===m.value ? '#e84c4a' : '#333', |
| 148 | fontWeight: recMode===m.value ? 600 : 400, |
| 149 | cursor: 'pointer', |
| 150 | transition: 'all 0.2s', |
| 151 | outline: 'none', |
| 152 | }} |
| 153 | >{m.label}</button> |
| 154 | ))} |
| 155 | </div> |
| 156 | {/* 协同过滤推荐数量选择 */} |
| 157 | {recMode === 'cf' && ( |
| 158 | <div style={{display:'flex',alignItems:'center',gap:4}}> |
| 159 | <span>推荐数量:</span> |
| 160 | <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'}}> |
| 161 | {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)} |
| 162 | </select> |
| 163 | </div> |
| 164 | )} |
| 165 | </div> |
| 166 | )} |
| 167 | {/* 搜索栏 */} |
| 168 | <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}> |
| 169 | <input |
| 170 | type="text" |
| 171 | className="search-input" |
| 172 | placeholder="搜索内容/标题/标签" |
| 173 | value={search} |
| 174 | onChange={e => setSearch(e.target.value)} |
| 175 | /> |
| 176 | <button type="submit" className="search-btn">搜索</button> |
| 177 | </form> |
| 178 | {/* 顶部分类 */} |
| 179 | <nav className="feed-tabs"> |
| 180 | {categories.map(cat => ( |
| 181 | <button |
| 182 | key={cat} |
| 183 | className={cat === activeCat ? 'tab active' : 'tab'} |
| 184 | onClick={() => { setActiveCat(cat); setSearch('') }} |
| 185 | > |
| 186 | {cat} |
| 187 | </button> |
| 188 | ))} |
| 189 | </nav> |
| 190 | {/* 瀑布流卡片区 */} |
| 191 | <div className="feed-grid"> |
| 192 | {loading ? <div style={{padding:32}}>加载中...</div> : |
| 193 | items.length === 0 ? <div style={{padding:32, color:'#aaa'}}>暂无推荐内容</div> : |
| 194 | items.map(item => ( |
22301008 | d5fbb78 | 2025-06-18 16:28:43 +0800 | [diff] [blame^] | 195 | <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}> |
22301008 | af17315 | 2025-06-15 10:46:25 +0800 | [diff] [blame] | 196 | {/* 封面图 */} |
| 197 | {/* <img className="card-img" src={item.img} alt={item.title} /> */} |
| 198 | {/* 标题 */} |
| 199 | <h3 className="card-title">{item.title}</h3> |
| 200 | {/* 内容摘要 */} |
| 201 | <div className="card-content">{item.content?.slice(0, 60) || ''}</div> |
| 202 | {/* 底部作者 + 点赞 */} |
| 203 | <div className="card-footer"> |
| 204 | <div className="card-author"> |
| 205 | {/* <img className="avatar" src={item.avatar} alt={item.author} /> */} |
| 206 | <span className="username">{item.author || '佚名'}</span> |
| 207 | </div> |
| 208 | <div className="card-likes"> |
| 209 | <ThumbsUp size={16} /> |
| 210 | <span className="likes-count">{item.heat || 0}</span> |
| 211 | </div> |
| 212 | </div> |
| 213 | </div> |
| 214 | ))} |
| 215 | </div> |
| 216 | </div> |
| 217 | ) |
| 218 | } |