blob: 3dcba27f800d5a3a9292b1cf15efe0f67b4a6fd7 [file] [log] [blame]
22301008af173152025-06-15 10:46:25 +08001import React, { useState, useEffect } from 'react'
2import { ThumbsUp } from 'lucide-react'
22301008d5fbb782025-06-18 16:28:43 +08003import { useNavigate } from 'react-router-dom'
22301008af173152025-06-15 10:46:25 +08004import '../style/HomeFeed.css'
5
6const categories = [
7 '推荐','穿搭','美食','彩妆','影视',
8 '职场','情感','家居','游戏','旅行','健身'
9]
10
11const recommendModes = [
12 { label: '标签推荐', value: 'tag' },
13 { label: '协同过滤推荐', value: 'cf' }
14]
15
16const DEFAULT_USER_ID = '3' // 确保数据库有此用户
17const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
18
19export default function HomeFeed() {
22301008d5fbb782025-06-18 16:28:43 +080020 const navigate = useNavigate()
22301008af173152025-06-15 10:46:25 +080021 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 {
22301008d5fbb782025-06-18 16:28:43 +080054 const res = await fetch(`http://127.0.0.1:5000/user_tags?user_id=${DEFAULT_USER_ID}`)
22301008af173152025-06-15 10:46:25 +080055 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 {
22301008d5fbb782025-06-18 16:28:43 +080073 const res = await fetch('http://127.0.0.1:5000/search', {
22301008af173152025-06-15 10:46:25 +080074 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 {
22301008d5fbb782025-06-18 16:28:43 +080090 const res = await fetch('http://127.0.0.1:5000/recommend_tags', {
22301008af173152025-06-15 10:46:25 +080091 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 {
22301008d5fbb782025-06-18 16:28:43 +0800107 const res = await fetch('http://127.0.0.1:5000/user_based_recommend', {
22301008af173152025-06-15 10:46:25 +0800108 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
22301008d5fbb782025-06-18 16:28:43 +0800125 const handlePostClick = (postId) => {
126 navigate(`/post/${postId}`)
127 }
128
22301008af173152025-06-15 10:46:25 +0800129 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 => (
22301008d5fbb782025-06-18 16:28:43 +0800195 <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
22301008af173152025-06-15 10:46:25 +0800196 {/* 封面图 */}
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}