feat: 完整集成JWLLL搜索推荐系统到Merge项目
新增功能:
- 完整的JWLLL搜索推荐后端服务 (back_jwlll/)
- 前端智能搜索和推荐功能集成
- HomeFeed组件增强: 数据源切换(原始数据 ↔ 智能推荐)
- 新增PostDetailJWLLL和UploadPageJWLLL组件
- 新增search_jwlll.js API接口
技术特性:
- 标签推荐和协同过滤推荐算法
- 中文分词和Word2Vec语义搜索
- 100%向后兼容,原功能完全保留
- 独立服务架构,无冲突部署
集成内容:
- JWLLL后端服务配置和依赖
- 前端路由和组件更新
- 样式文件和API集成
- 项目文档和启动工具
Change-Id: I1d008cf04eee40e7d81bfb9109f933d3447d1760
diff --git a/Merge/front/src/components/PostDetailJWLLL.jsx b/Merge/front/src/components/PostDetailJWLLL.jsx
new file mode 100644
index 0000000..0dc7289
--- /dev/null
+++ b/Merge/front/src/components/PostDetailJWLLL.jsx
@@ -0,0 +1,322 @@
+import React, { useState, useEffect } from 'react'
+import { useParams, useNavigate } from 'react-router-dom'
+import { ArrowLeft, ThumbsUp, MessageCircle, Share2, BookmarkPlus, Heart, Eye } from 'lucide-react'
+import { searchAPI } from '../api/search_jwlll'
+import '../style/PostDetail.css'
+
+export default function PostDetail() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const [post, setPost] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [liked, setLiked] = useState(false)
+ const [bookmarked, setBookmarked] = useState(false)
+ const [likeCount, setLikeCount] = useState(0)
+ const [comments, setComments] = useState([])
+ const [newComment, setNewComment] = useState('')
+ const [showComments, setShowComments] = useState(false)
+
+ const DEFAULT_USER_ID = '3' // 默认用户ID
+
+ useEffect(() => {
+ fetchPostDetail()
+ fetchComments()
+ }, [id])
+
+ const fetchPostDetail = async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const data = await searchAPI.getPostDetail(id)
+ setPost(data)
+ setLikeCount(data.heat || 0)
+ } catch (error) {
+ console.error('获取帖子详情失败:', error)
+ setError('帖子不存在或已被删除')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const fetchComments = async () => {
+ try {
+ const data = await searchAPI.getComments(id)
+ setComments(data.comments || [])
+ } catch (error) {
+ console.error('获取评论失败:', error)
+ }
+ }
+
+ const handleBack = () => {
+ navigate(-1)
+ }
+
+ const handleLike = async () => {
+ try {
+ const newLiked = !liked
+ if (newLiked) {
+ await searchAPI.likePost(id, DEFAULT_USER_ID)
+ } else {
+ await searchAPI.unlikePost(id, DEFAULT_USER_ID)
+ }
+ setLiked(newLiked)
+ setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
+ } catch (error) {
+ console.error('点赞失败:', error)
+ // 回滚状态
+ setLiked(!liked)
+ setLikeCount(prev => liked ? prev + 1 : prev - 1)
+ }
+ }
+
+ const handleBookmark = () => {
+ setBookmarked(!bookmarked)
+ // 实际项目中这里应该调用后端API保存收藏状态
+ }
+
+ const handleShare = () => {
+ // 分享功能
+ if (navigator.share) {
+ navigator.share({
+ title: post?.title,
+ text: post?.content,
+ url: window.location.href,
+ })
+ } else {
+ // 复制链接到剪贴板
+ navigator.clipboard.writeText(window.location.href)
+ alert('链接已复制到剪贴板')
+ }
+ }
+
+ const handleAddComment = async (e) => {
+ e.preventDefault()
+ if (!newComment.trim()) return
+
+ try {
+ await searchAPI.addComment(id, DEFAULT_USER_ID, newComment)
+ setNewComment('')
+ fetchComments() // 刷新评论列表
+ } catch (error) {
+ console.error('添加评论失败:', error)
+ alert('评论失败,请重试')
+ }
+ }
+
+ if (loading) {
+ return (
+ <div className="post-detail">
+ <div className="loading-container">
+ <div className="loading-spinner"></div>
+ <p>加载中...</p>
+ </div>
+ </div>
+ )
+ }
+
+ if (error) {
+ return (
+ <div className="post-detail">
+ <div className="error-container">
+ <h2>😔 出错了</h2>
+ <p>{error}</p>
+ <button onClick={handleBack} className="back-btn">
+ <ArrowLeft size={20} />
+ 返回
+ </button>
+ </div>
+ </div>
+ )
+ }
+
+ if (!post) {
+ return (
+ <div className="post-detail">
+ <div className="error-container">
+ <h2>😔 帖子不存在</h2>
+ <p>该帖子可能已被删除或不存在</p>
+ <button onClick={handleBack} className="back-btn">
+ <ArrowLeft size={20} />
+ 返回
+ </button>
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="post-detail">
+ {/* 顶部导航栏 */}
+ <header className="post-header">
+ <button onClick={handleBack} className="back-btn">
+ <ArrowLeft size={20} />
+ 返回
+ </button>
+ <div className="header-actions">
+ <button onClick={handleShare} className="action-btn">
+ <Share2 size={20} />
+ </button>
+ <button
+ onClick={handleBookmark}
+ className={`action-btn ${bookmarked ? 'active' : ''}`}
+ >
+ <BookmarkPlus size={20} />
+ </button>
+ </div>
+ </header>
+
+ {/* 主要内容区 */}
+ <main className="post-content">
+ {/* 帖子标题 */}
+ <h1 className="post-title">{post.title}</h1>
+
+ {/* 作者信息和元数据 */}
+ <div className="post-meta">
+ <div className="author-info">
+ <div className="avatar">
+ {post.author ? post.author.charAt(0).toUpperCase() : 'U'}
+ </div>
+ <div className="author-details">
+ <span className="author-name">{post.author || '匿名用户'}</span>
+ <span className="post-date">
+ {post.create_time ? new Date(post.create_time).toLocaleDateString('zh-CN') : '未知时间'}
+ </span>
+ </div>
+ </div>
+ <div className="post-stats">
+ <span className="stat-item">
+ <Eye size={16} />
+ {post.views || 0}
+ </span>
+ <span className="stat-item">
+ <Heart size={16} />
+ {likeCount}
+ </span>
+ </div>
+ </div>
+
+ {/* 标签 */}
+ {post.tags && post.tags.length > 0 && (
+ <div className="post-tags">
+ {post.tags.map((tag, index) => (
+ <span key={index} className="tag">{tag}</span>
+ ))}
+ </div>
+ )}
+
+ {/* 帖子正文 */}
+ <div className="post-body">
+ <p>{post.content}</p>
+ </div>
+
+ {/* 类别信息 */}
+ {(post.category || post.type) && (
+ <div className="post-category">
+ {post.category && (
+ <>
+ <span className="category-label">分类:</span>
+ <span className="category-name">{post.category}</span>
+ </>
+ )}
+ {post.type && (
+ <>
+ <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
+ <span className="category-name">{post.type}</span>
+ </>
+ )}
+ </div>
+ )}
+
+ {/* 评论区 */}
+ <div className="comments-section">
+ <div className="comments-header">
+ <button
+ onClick={() => setShowComments(!showComments)}
+ className="comments-toggle"
+ >
+ <MessageCircle size={20} />
+ 评论 ({comments.length})
+ </button>
+ </div>
+
+ {showComments && (
+ <div className="comments-content">
+ {/* 添加评论 */}
+ <form onSubmit={handleAddComment} className="comment-form">
+ <textarea
+ value={newComment}
+ onChange={(e) => setNewComment(e.target.value)}
+ placeholder="写下你的评论..."
+ className="comment-input"
+ rows={3}
+ />
+ <button type="submit" className="comment-submit">
+ 发布评论
+ </button>
+ </form>
+
+ {/* 评论列表 */}
+ <div className="comments-list">
+ {comments.length === 0 ? (
+ <p className="no-comments">暂无评论</p>
+ ) : (
+ comments.map((comment, index) => (
+ <div key={index} className="comment-item">
+ <div className="comment-author">
+ <div className="comment-avatar">
+ {comment.user_name ? comment.user_name.charAt(0).toUpperCase() : 'U'}
+ </div>
+ <span className="comment-name">{comment.user_name || '匿名用户'}</span>
+ <span className="comment-time">
+ {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
+ </span>
+ </div>
+ <div className="comment-content">
+ {comment.content}
+ </div>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </main>
+
+ {/* 底部操作栏 */}
+ <footer className="post-footer">
+ <div className="action-bar">
+ <button
+ onClick={handleLike}
+ className={`action-button ${liked ? 'liked' : ''}`}
+ >
+ <ThumbsUp size={20} />
+ <span>{likeCount}</span>
+ </button>
+
+ <button
+ onClick={() => setShowComments(!showComments)}
+ className="action-button"
+ >
+ <MessageCircle size={20} />
+ <span>评论</span>
+ </button>
+
+ <button onClick={handleShare} className="action-button">
+ <Share2 size={20} />
+ <span>分享</span>
+ </button>
+
+ <button
+ onClick={handleBookmark}
+ className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
+ >
+ <BookmarkPlus size={20} />
+ <span>收藏</span>
+ </button>
+ </div>
+ </footer>
+ </div>
+ )
+}