TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 1 | // src/components/HomeFeed.jsx |
| 2 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 3 | import React, { useState, useEffect, useCallback } from 'react' |
TRM-coding | 29174c2 | 2025-06-18 23:56:51 +0800 | [diff] [blame] | 4 | import { useNavigate } from 'react-router-dom' |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 5 | import { ThumbsUp } from 'lucide-react' |
| 6 | import { fetchPosts, fetchPost } from '../api/posts_wzy' |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 7 | import { searchAPI } from '../api/search_jwlll' |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 8 | import { getUserInfo } from '../utils/auth' |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 9 | import { deepRecommend } from '../api/recommend_rhj' |
22301008 | e25b4b0 | 2025-06-20 22:15:31 +0800 | [diff] [blame] | 10 | import postsAPI from '../api/posts_api' |
956303669 | 9de8c09 | 2025-06-21 16:41:18 +0800 | [diff] [blame] | 11 | import MediaPreview from './MediaPreview' |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 12 | import '../style/HomeFeed.css' |
| 13 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 14 | const recommendModes = [ |
| 15 | { label: '标签推荐', value: 'tag' }, |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 16 | { label: '协同过滤推荐', value: 'cf' }, |
| 17 | { label: '深度推荐', value: 'deep' } // 新增 |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 18 | ] |
| 19 | |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 20 | const DEFAULT_USER_ID = '3' // 确保数据库有此用户(作为回退值) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 21 | const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整 |
| 22 | |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 23 | export default function HomeFeed() { |
TRM-coding | 29174c2 | 2025-06-18 23:56:51 +0800 | [diff] [blame] | 24 | const navigate = useNavigate() |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 25 | |
| 26 | // 获取当前用户ID,如果未登录则使用默认值 |
| 27 | const getCurrentUserId = () => { |
| 28 | const userInfo = getUserInfo() |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 29 | return userInfo?.id ? String(userInfo.id) : DEFAULT_USER_ID |
| 30 | } |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 31 | |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 32 | const [items, setItems] = useState([]) |
| 33 | const [loading, setLoading] = useState(true) |
| 34 | const [error, setError] = useState(null) |
| 35 | // JWLLL 搜索推荐相关状态 |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 36 | const [search, setSearch] = useState('') |
| 37 | const [recMode, setRecMode] = useState('tag') |
| 38 | const [recCFNum, setRecCFNum] = useState(20) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 39 | const [useSearchRecommend, setUseSearchRecommend] = useState(false) |
| 40 | const [userMap, setUserMap] = useState({}) |
| 41 | |
| 42 | // 异步加载单个帖子的详细信息 |
| 43 | const loadPostDetails = async (postId, index) => { |
| 44 | try { |
| 45 | const d = await fetchPost(postId) |
| 46 | setItems(prevItems => { |
| 47 | const newItems = [...prevItems] |
| 48 | if (newItems[index]) { |
| 49 | newItems[index] = { |
| 50 | ...newItems[index], |
| 51 | media: d.media_urls?.[0] || '', |
| 52 | content: d.content || '', |
| 53 | mediaUrls: d.media_urls || [], |
| 54 | author: `作者 ${d.user_id}`, |
| 55 | authorId: d.user_id, |
| 56 | avatar: `http://192.168.5.200:8080/static/profile.webp`, |
| 57 | detailsLoaded: true |
| 58 | } |
| 59 | } |
| 60 | return newItems |
| 61 | }) |
| 62 | } catch (error) { |
| 63 | console.error(`加载帖子 ${postId} 详情失败:`, error) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // JWLLL搜索推荐内容 |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 68 | const fetchSearchContent = useCallback(async (keyword = '') => { |
| 69 | setLoading(true) |
| 70 | setError(null) |
| 71 | try { |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 72 | const data = await searchAPI.search(keyword, undefined) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 73 | // 先设置基础数据,快速显示 |
| 74 | const basicItems = (data.results || []).map(item => ({ |
| 75 | id: item.id, |
| 76 | title: item.title, |
| 77 | author: item.author || '佚名', |
| 78 | avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| 79 | media: item.img || '', |
| 80 | likes: item.heat || 0, |
| 81 | content: item.content || '', |
| 82 | mediaUrls: [], |
| 83 | detailsLoaded: false |
| 84 | })) |
| 85 | setItems(basicItems) |
| 86 | setLoading(false) |
| 87 | |
| 88 | // 异步加载详细信息 |
| 89 | basicItems.forEach((item, index) => { |
| 90 | loadPostDetails(item.id, index) |
| 91 | }) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 92 | } catch (e) { |
| 93 | console.error('搜索失败:', e) |
| 94 | setError('搜索失败') |
| 95 | setItems([]) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 96 | setLoading(false) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 97 | } |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 98 | }, []) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 99 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 100 | // 标签推荐 |
| 101 | const fetchTagRecommend = useCallback(async (tags) => { |
| 102 | setLoading(true) |
| 103 | setError(null) |
| 104 | try { |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 105 | const currentUserId = getCurrentUserId() |
| 106 | const data = await searchAPI.recommendByTags(currentUserId, tags) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 107 | // 先设置基础数据,快速显示 |
| 108 | const basicItems = (data.recommendations || []).map(item => ({ |
| 109 | id: item.id, |
| 110 | title: item.title, |
| 111 | author: item.author || '佚名', |
| 112 | avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| 113 | media: item.img || '', |
| 114 | likes: item.heat || 0, |
| 115 | content: item.content || '', |
| 116 | mediaUrls: [], |
| 117 | detailsLoaded: false |
| 118 | })) |
| 119 | setItems(basicItems) |
| 120 | setLoading(false) |
| 121 | |
| 122 | // 异步加载详细信息 |
| 123 | basicItems.forEach((item, index) => { |
| 124 | loadPostDetails(item.id, index) |
| 125 | }) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 126 | } catch (e) { |
| 127 | console.error('标签推荐失败:', e) |
| 128 | setError('标签推荐失败') |
| 129 | setItems([]) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 130 | setLoading(false) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 131 | } |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 132 | }, []) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 133 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 134 | // 协同过滤推荐 |
| 135 | const fetchCFRecommend = useCallback(async (topN = recCFNum) => { |
| 136 | setLoading(true) |
| 137 | setError(null) |
| 138 | try { |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 139 | const currentUserId = getCurrentUserId() |
| 140 | const data = await searchAPI.userBasedRecommend(currentUserId, topN) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 141 | // 先设置基础数据,快速显示 |
| 142 | const basicItems = (data.recommendations || []).map(item => ({ |
| 143 | id: item.id, |
| 144 | title: item.title, |
| 145 | author: item.author || '佚名', |
| 146 | avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| 147 | media: item.img || '', |
| 148 | likes: item.heat || 0, |
| 149 | content: item.content || '', |
| 150 | mediaUrls: [], |
| 151 | detailsLoaded: false |
| 152 | })) |
| 153 | setItems(basicItems) |
| 154 | setLoading(false) |
| 155 | |
| 156 | // 异步加载详细信息 |
| 157 | basicItems.forEach((item, index) => { |
| 158 | loadPostDetails(item.id, index) |
| 159 | }) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 160 | } catch (e) { |
| 161 | console.error('协同过滤推荐失败:', e) |
| 162 | setError('协同过滤推荐失败') |
| 163 | setItems([]) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 164 | setLoading(false) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 165 | } |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 166 | }, [recCFNum]) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 167 | |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 168 | // 深度推荐 |
| 169 | const fetchDeepRecommend = useCallback(async (topN = 20) => { |
| 170 | setLoading(true) |
| 171 | setError(null) |
| 172 | try { |
| 173 | const currentUserId = getCurrentUserId() |
| 174 | const recs = await deepRecommend(currentUserId, topN) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 175 | // 先设置基础数据,快速显示 |
| 176 | const basicItems = (recs || []).map(item => ({ |
| 177 | id: item.id, |
| 178 | title: item.title, |
| 179 | author: item.author || '佚名', |
| 180 | avatar: `https://i.pravatar.cc/40?img=${item.id}`, |
| 181 | media: item.img || '', |
| 182 | likes: item.heat || 0, |
| 183 | content: item.content || '', |
| 184 | mediaUrls: [], |
| 185 | detailsLoaded: false |
| 186 | })) |
| 187 | setItems(basicItems) |
| 188 | setLoading(false) |
| 189 | |
| 190 | // 异步加载详细信息 |
| 191 | basicItems.forEach((item, index) => { |
| 192 | loadPostDetails(item.id, index) |
| 193 | }) |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 194 | } catch (e) { |
| 195 | setError('深度推荐失败') |
| 196 | setItems([]) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 197 | setLoading(false) |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 198 | } |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 199 | }, []) |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 200 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 201 | // 获取用户兴趣标签后再推荐 |
| 202 | const fetchUserTagsAndRecommend = useCallback(async () => { |
| 203 | setLoading(true) |
| 204 | setError(null) |
| 205 | let tags = [] |
| 206 | try { |
22301008 | ba662fe | 2025-06-20 18:10:20 +0800 | [diff] [blame] | 207 | const currentUserId = getCurrentUserId() |
| 208 | const data = await searchAPI.getUserTags(currentUserId) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 209 | tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS |
| 210 | } catch { |
| 211 | tags = DEFAULT_TAGS |
| 212 | } |
| 213 | if (recMode === 'tag') { |
| 214 | await fetchTagRecommend(tags) |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 215 | } else if (recMode === 'cf') { |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 216 | await fetchCFRecommend() |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 217 | } else if (recMode === 'deep') { |
| 218 | await fetchDeepRecommend() |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 219 | } |
| 220 | setLoading(false) |
22301008 | b86c21c | 2025-06-20 19:17:00 +0800 | [diff] [blame] | 221 | }, [recMode, fetchTagRecommend, fetchCFRecommend, fetchDeepRecommend]) |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 222 | |
22301008 | e25b4b0 | 2025-06-20 22:15:31 +0800 | [diff] [blame] | 223 | // 拉取所有涉及用户的昵称 |
| 224 | const fetchUserNames = async (userIds) => { |
| 225 | const map = {} |
| 226 | await Promise.all(userIds.map(async uid => { |
| 227 | try { |
| 228 | const user = await postsAPI.getUser(uid) |
| 229 | map[uid] = user.username || user.nickname || `用户${uid}` |
| 230 | } catch { |
| 231 | map[uid] = `用户${uid}` |
| 232 | } |
| 233 | })) |
| 234 | setUserMap(map) |
| 235 | } |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 236 | useEffect(() => { |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 237 | // 原始数据加载函数 |
| 238 | const loadPosts = async () => { |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 239 | try { |
| 240 | const list = await fetchPosts() // [{id, title, heat, created_at}, …] |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 241 | // 先设置基础数据,快速显示 |
| 242 | const basicItems = list.map(p => ({ |
| 243 | id: p.id, |
| 244 | title: p.title, |
| 245 | author: '加载中...', |
| 246 | authorId: null, |
| 247 | avatar: `http://192.168.5.200:8080/static/profile.webp`, |
| 248 | media: '', |
| 249 | likes: p.heat, |
| 250 | mediaUrls: [], |
| 251 | detailsLoaded: false |
| 252 | })) |
| 253 | setItems(basicItems) |
| 254 | setLoading(false) |
| 255 | |
| 256 | // 异步加载详细信息 |
| 257 | basicItems.forEach((item, index) => { |
| 258 | loadPostDetails(item.id, index) |
| 259 | }) |
| 260 | |
| 261 | // 异步加载用户信息 |
| 262 | setTimeout(async () => { |
| 263 | const allItems = await Promise.all( |
| 264 | list.map(async p => { |
| 265 | try { |
| 266 | const d = await fetchPost(p.id) |
| 267 | return { ...p, user_id: d.user_id } |
| 268 | } catch { |
| 269 | return p |
| 270 | } |
| 271 | }) |
| 272 | ) |
| 273 | const userIds = [...new Set(allItems.map(i => i.user_id).filter(Boolean))] |
| 274 | fetchUserNames(userIds) |
| 275 | }, 100) |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 276 | } catch (e) { |
| 277 | setError(e.message) |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 278 | setLoading(false) |
| 279 | } |
| 280 | } |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 281 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 282 | // 根据模式选择加载方式 |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 283 | if (useSearchRecommend) { |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 284 | fetchUserTagsAndRecommend() |
| 285 | } else { |
| 286 | loadPosts() |
| 287 | } |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 288 | }, [useSearchRecommend, fetchUserTagsAndRecommend]) |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 289 | useEffect(() => { |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 290 | if (useSearchRecommend) { |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 291 | fetchUserTagsAndRecommend() |
| 292 | } |
| 293 | // eslint-disable-next-line |
| 294 | }, [recMode, fetchUserTagsAndRecommend]) |
| 295 | |
| 296 | // 根据模式选择不同的加载方式 |
| 297 | const handleSearch = e => { |
| 298 | e.preventDefault() |
| 299 | if (useSearchRecommend) { |
| 300 | fetchSearchContent(search) |
| 301 | } else { |
| 302 | // 切换到搜索推荐模式 |
| 303 | setUseSearchRecommend(true) |
| 304 | fetchSearchContent(search) |
| 305 | } |
| 306 | } |
| 307 | |
956303669 | 9de8c09 | 2025-06-21 16:41:18 +0800 | [diff] [blame] | 308 | const [previewImg, setPreviewImg] = useState(null) |
| 309 | |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 310 | const handlePostClick = (postId) => { |
| 311 | navigate(`/post/${postId}`) |
| 312 | } |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 313 | return ( |
| 314 | <div className="home-feed"> |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 315 | {/* 数据源切换 */} |
| 316 | <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}> |
| 317 | <span>数据源:</span> |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 318 | <div style={{display:'flex', gap:8}}> <button |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 319 | className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'} |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 320 | onClick={() => setUseSearchRecommend(false)} |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 321 | type="button" |
| 322 | style={{ |
| 323 | borderRadius: 20, |
| 324 | padding: '4px 18px', |
| 325 | border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc', |
| 326 | background: !useSearchRecommend ? '#fff0f0' : '#fff', |
| 327 | color: !useSearchRecommend ? '#e84c4a' : '#333', |
| 328 | fontWeight: !useSearchRecommend ? 600 : 400, |
| 329 | cursor: 'pointer', |
| 330 | transition: 'all 0.2s', |
| 331 | outline: 'none', |
| 332 | }} |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 333 | >原始数据</button> <button |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 334 | className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'} |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 335 | onClick={() => setUseSearchRecommend(true)} |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 336 | type="button" |
| 337 | style={{ |
| 338 | borderRadius: 20, |
| 339 | padding: '4px 18px', |
| 340 | border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc', |
| 341 | background: useSearchRecommend ? '#fff0f0' : '#fff', |
| 342 | color: useSearchRecommend ? '#e84c4a' : '#333', |
| 343 | fontWeight: useSearchRecommend ? 600 : 400, |
| 344 | cursor: 'pointer', |
| 345 | transition: 'all 0.2s', |
| 346 | outline: 'none', |
| 347 | }} |
| 348 | >智能推荐</button> |
| 349 | </div> |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 350 | </div> {/* 推荐模式切换,仅在使用智能推荐时显示 */} |
| 351 | {useSearchRecommend && ( |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 352 | <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}> |
| 353 | <span style={{marginRight:8}}>推荐模式:</span> |
| 354 | <div style={{display:'flex', gap:8}}> |
| 355 | {recommendModes.map(m => ( |
| 356 | <button |
| 357 | key={m.value} |
| 358 | className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'} |
| 359 | onClick={() => setRecMode(m.value)} |
| 360 | type="button" |
| 361 | style={{ |
| 362 | borderRadius: 20, |
| 363 | padding: '4px 18px', |
| 364 | border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc', |
| 365 | background: recMode===m.value ? '#fff0f0' : '#fff', |
| 366 | color: recMode===m.value ? '#e84c4a' : '#333', |
| 367 | fontWeight: recMode===m.value ? 600 : 400, |
| 368 | cursor: 'pointer', |
| 369 | transition: 'all 0.2s', |
| 370 | outline: 'none', |
| 371 | }} |
| 372 | >{m.label}</button> |
| 373 | ))} |
| 374 | </div> |
| 375 | {/* 协同过滤推荐数量选择 */} |
| 376 | {recMode === 'cf' && ( |
| 377 | <div style={{display:'flex',alignItems:'center',gap:4}}> |
| 378 | <span>推荐数量:</span> |
| 379 | <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'}}> |
| 380 | {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)} |
| 381 | </select> |
| 382 | </div> |
| 383 | )} |
| 384 | </div> |
| 385 | )} |
| 386 | |
| 387 | {/* 搜索栏 */} |
| 388 | <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}> |
| 389 | <input |
| 390 | type="text" |
| 391 | className="search-input" |
| 392 | placeholder="搜索内容/标题/标签" |
| 393 | value={search} |
| 394 | onChange={e => setSearch(e.target.value)} |
| 395 | /> |
| 396 | <button type="submit" className="search-btn">搜索</button> |
22301008 | 661ef47 | 2025-06-26 18:58:48 +0800 | [diff] [blame] | 397 | </form> {/* 状态提示 */} |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 398 | {loading ? ( |
| 399 | <div className="loading">加载中…</div> |
| 400 | ) : error ? ( |
| 401 | <div className="error">加载失败:{error}</div> |
| 402 | ) : ( |
| 403 | /* 瀑布流卡片区 */ |
| 404 | <div className="feed-grid"> |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 405 | {items.length === 0 ? ( |
| 406 | <div style={{padding:32, color:'#aaa'}}>暂无内容</div> |
| 407 | ) : ( |
| 408 | items.map(item => ( |
| 409 | <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}> |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 410 | {item.media ? ( |
956303669 | 9de8c09 | 2025-06-21 16:41:18 +0800 | [diff] [blame] | 411 | <MediaPreview |
| 412 | url={item.media} |
| 413 | alt={item.title} |
| 414 | className="card-img" |
| 415 | onClick={(url) => { |
| 416 | // 对于图片,显示预览 |
| 417 | if (!url.toLowerCase().includes('video') && !url.includes('.mp4') && !url.includes('.webm')) { |
| 418 | setPreviewImg(url) |
| 419 | } |
| 420 | }} |
| 421 | style={{ cursor: 'pointer' }} |
| 422 | /> |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 423 | ) : !item.detailsLoaded ? ( |
| 424 | <div className="card-img-placeholder" style={{ |
| 425 | height: '200px', |
| 426 | background: '#f5f5f5', |
| 427 | display: 'flex', |
| 428 | alignItems: 'center', |
| 429 | justifyContent: 'center', |
| 430 | color: '#999', |
| 431 | fontSize: '14px' |
| 432 | }}> |
| 433 | 加载中... |
| 434 | </div> |
| 435 | ) : null} |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 436 | <h3 className="card-title">{item.title}</h3> |
| 437 | {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>} |
| 438 | <div className="card-footer"> |
| 439 | <div className="card-author"> |
trm | 275c600 | 2025-06-27 15:37:32 +0000 | [diff] [blame^] | 440 | <img className="avatar" src={item.avatar} alt={userMap[item.authorId] || item.authorId || item.author} /> |
| 441 | <span className="username">{userMap[item.authorId] || item.author}</span> |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 442 | </div> |
| 443 | <div className="card-likes"> |
| 444 | <ThumbsUp size={16} /> |
| 445 | <span className="likes-count">{item.likes}</span> |
| 446 | </div> |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 447 | </div> |
| 448 | </div> |
956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame] | 449 | )) |
| 450 | )} |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 451 | </div> |
| 452 | )} |
956303669 | 9de8c09 | 2025-06-21 16:41:18 +0800 | [diff] [blame] | 453 | |
| 454 | {/* 图片预览弹窗 */} |
| 455 | {previewImg && ( |
| 456 | <div |
| 457 | className="img-preview-mask" |
| 458 | style={{ |
| 459 | position: 'fixed', |
| 460 | zIndex: 9999, |
| 461 | top: 0, |
| 462 | left: 0, |
| 463 | right: 0, |
| 464 | bottom: 0, |
| 465 | background: 'rgba(0,0,0,0.7)', |
| 466 | display: 'flex', |
| 467 | alignItems: 'center', |
| 468 | justifyContent: 'center' |
| 469 | }} |
| 470 | onClick={() => setPreviewImg(null)} |
| 471 | > |
| 472 | <img |
| 473 | src={previewImg} |
| 474 | alt="大图预览" |
| 475 | style={{ |
| 476 | maxWidth: '90vw', |
| 477 | maxHeight: '90vh', |
| 478 | borderRadius: 12, |
| 479 | boxShadow: '0 4px 24px #0008' |
| 480 | }} |
| 481 | /> |
| 482 | </div> |
| 483 | )} |
TRM-coding | d1cbf67 | 2025-06-18 15:15:08 +0800 | [diff] [blame] | 484 | </div> |
| 485 | ) |
| 486 | } |