blob: 1fb12a53b54d55b3aac00a8c0e04432a96aaff66 [file] [log] [blame]
// 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 '../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 搜索推荐功能函数
// JWLLL搜索推荐内容
const fetchSearchContent = useCallback(async (keyword = '') => {
setLoading(true)
setError(null)
try {
const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
const formattedItems = (data.results || []).map(item => ({
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(formattedItems)
} 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])
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)
} 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={item.author} />
<span className="username">{item.author}</span>
</div>
<div className="card-likes">
<ThumbsUp size={16} />
<span className="likes-count">{item.likes}</span>
</div>
</div>
</div>
))
)}
</div>
)}
</div>
)
}