增加帖子详情api与前端页面,需完善按钮与显示
Change-Id: I84d3aace81055b8dc372f91942523d163b1ec463
diff --git a/JWLLL/API_front/src/components/HomeFeed.jsx b/JWLLL/API_front/src/components/HomeFeed.jsx
index 0b17b1f..3dcba27 100644
--- a/JWLLL/API_front/src/components/HomeFeed.jsx
+++ b/JWLLL/API_front/src/components/HomeFeed.jsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { ThumbsUp } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
import '../style/HomeFeed.css'
const categories = [
@@ -16,6 +17,7 @@
const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
export default function HomeFeed() {
+ const navigate = useNavigate()
const [activeCat, setActiveCat] = useState('推荐')
const [items, setItems] = useState([])
const [search, setSearch] = useState('')
@@ -49,7 +51,7 @@
setLoading(true)
let tags = []
try {
- const res = await fetch(`/user_tags?user_id=${DEFAULT_USER_ID}`)
+ const res = await fetch(`http://127.0.0.1:5000/user_tags?user_id=${DEFAULT_USER_ID}`)
const data = await res.json()
tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
setUserTags(tags)
@@ -68,7 +70,7 @@
const fetchContent = async (keyword = '') => {
setLoading(true)
try {
- const res = await fetch('/search', {
+ const res = await fetch('http://127.0.0.1:5000/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword: keyword || activeCat, category: activeCat === '推荐' ? undefined : activeCat })
@@ -85,7 +87,7 @@
const fetchTagRecommend = async (tags) => {
setLoading(true)
try {
- const res = await fetch('/recommend_tags', {
+ const res = await fetch('http://127.0.0.1:5000/recommend_tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: DEFAULT_USER_ID, tags })
@@ -102,7 +104,7 @@
const fetchCFRecommend = async (topN = recCFNum) => {
setLoading(true)
try {
- const res = await fetch('/user_based_recommend', {
+ const res = await fetch('http://127.0.0.1:5000/user_based_recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: DEFAULT_USER_ID, top_n: topN })
@@ -120,6 +122,10 @@
fetchContent(search)
}
+ const handlePostClick = (postId) => {
+ navigate(`/post/${postId}`)
+ }
+
return (
<div className="home-feed">
{/* 推荐模式切换,仅在推荐页显示 */}
@@ -186,7 +192,7 @@
{loading ? <div style={{padding:32}}>加载中...</div> :
items.length === 0 ? <div style={{padding:32, color:'#aaa'}}>暂无推荐内容</div> :
items.map(item => (
- <div key={item.id} className="feed-card">
+ <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
{/* 封面图 */}
{/* <img className="card-img" src={item.img} alt={item.title} /> */}
{/* 标题 */}
diff --git a/JWLLL/API_front/src/components/PostDetail.jsx b/JWLLL/API_front/src/components/PostDetail.jsx
new file mode 100644
index 0000000..f7506ac
--- /dev/null
+++ b/JWLLL/API_front/src/components/PostDetail.jsx
@@ -0,0 +1,227 @@
+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 '../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)
+ useEffect(() => {
+ fetchPostDetail()
+ }, [id])
+
+ const fetchPostDetail = async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ // 根据ID获取帖子详情,使用本地服务器地址
+ const response = await fetch(`http://127.0.0.1:5000/post/${id}`)
+ if (!response.ok) {
+ throw new Error('帖子不存在或已被删除')
+ }
+ const data = await response.json()
+ setPost(data)
+ setLikeCount(data.heat || 0)
+ } catch (error) {
+ console.error('获取帖子详情失败:', error)
+ setError(error.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleBack = () => {
+ navigate(-1)
+ }
+
+ const handleLike = async () => {
+ try {
+ // 模拟点赞API调用
+ const newLiked = !liked
+ setLiked(newLiked)
+ setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
+
+ // 实际项目中这里应该调用后端API
+ // await fetch(`/post/${id}/like`, { method: 'POST' })
+ } 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('链接已复制到剪贴板')
+ }
+ }
+
+ 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 && (
+ <div className="post-category">
+ <span className="category-label">分类:</span>
+ <span className="category-name">{post.category}</span>
+ </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 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>
+ )
+}
diff --git a/JWLLL/API_front/src/router/index.jsx b/JWLLL/API_front/src/router/index.jsx
index 64191ac..023e8f6 100644
--- a/JWLLL/API_front/src/router/index.jsx
+++ b/JWLLL/API_front/src/router/index.jsx
@@ -3,6 +3,7 @@
import UploadPage from '../components/UploadPage'
import PlaceholderPage from '../components/PlaceholderPage'
import HomeFeed from '../components/HomeFeed'
+import PostDetail from '../components/PostDetail'
export default function AppRouter() {
return (
@@ -10,6 +11,7 @@
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/home" element={<HomeFeed />} />
+ <Route path="/post/:id" element={<PostDetail />} />
<Route path="/notebooks" element={<PlaceholderPage pageId="notebooks" />} />
<Route path="/activity" element={<PlaceholderPage pageId="activity" />} />
<Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
diff --git a/JWLLL/API_front/src/style/HomeFeed.css b/JWLLL/API_front/src/style/HomeFeed.css
index 6cd353e..3ba1232 100644
--- a/JWLLL/API_front/src/style/HomeFeed.css
+++ b/JWLLL/API_front/src/style/HomeFeed.css
@@ -40,11 +40,13 @@
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
- transition: transform 0.2s;
+ transition: transform 0.2s, box-shadow 0.2s;
+ cursor: pointer; /* 添加手型指针 */
}
.feed-card:hover {
transform: translateY(-4px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* 封面图固定高度 */
diff --git a/JWLLL/API_front/src/style/PostDetail.css b/JWLLL/API_front/src/style/PostDetail.css
new file mode 100644
index 0000000..6257670
--- /dev/null
+++ b/JWLLL/API_front/src/style/PostDetail.css
@@ -0,0 +1,322 @@
+/* 帖子详情页面容器 */
+.post-detail {
+ max-width: 800px;
+ margin: 0 auto;
+ background: #fff;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* 加载状态 */
+.loading-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ color: #666;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #ff4757;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 20px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* 错误状态 */
+.error-container {
+ text-align: center;
+ padding: 60px 20px;
+ color: #666;
+}
+
+.error-container h2 {
+ margin-bottom: 16px;
+ color: #333;
+}
+
+/* 顶部导航栏 */
+.post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid #eee;
+ background: #fff;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.back-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ border: none;
+ background: #f8f9fa;
+ border-radius: 20px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ font-size: 14px;
+ color: #333;
+}
+
+.back-btn:hover {
+ background: #e9ecef;
+}
+
+.header-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: #f8f9fa;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ color: #666;
+}
+
+.action-btn:hover {
+ background: #e9ecef;
+}
+
+.action-btn.active {
+ background: #ff4757;
+ color: #fff;
+}
+
+/* 主要内容区 */
+.post-content {
+ flex: 1;
+ padding: 20px;
+}
+
+/* 帖子标题 */
+.post-title {
+ font-size: 28px;
+ font-weight: 700;
+ line-height: 1.3;
+ margin-bottom: 20px;
+ color: #2c3e50;
+}
+
+/* 作者信息和元数据 */
+.post-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+}
+
+.author-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #ff4757, #ff6b7a);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.author-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.author-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.post-date {
+ font-size: 14px;
+ color: #7f8c8d;
+}
+
+.post-stats {
+ display: flex;
+ gap: 16px;
+}
+
+.stat-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: #7f8c8d;
+ font-size: 14px;
+}
+
+/* 标签 */
+.post-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 24px;
+}
+
+.tag {
+ padding: 4px 12px;
+ background: #f8f9fa;
+ border-radius: 12px;
+ font-size: 12px;
+ color: #666;
+ border: 1px solid #dee2e6;
+}
+
+/* 帖子正文 */
+.post-body {
+ line-height: 1.8;
+ font-size: 16px;
+ color: #2c3e50;
+ margin-bottom: 24px;
+}
+
+.post-body p {
+ margin-bottom: 16px;
+}
+
+/* 类别信息 */
+.post-category {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.category-label {
+ font-size: 14px;
+ color: #7f8c8d;
+}
+
+.category-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: #ff4757;
+}
+
+/* 底部操作栏 */
+.post-footer {
+ border-top: 1px solid #eee;
+ background: #fff;
+ position: sticky;
+ bottom: 0;
+}
+
+.action-bar {
+ display: flex;
+ justify-content: space-around;
+ padding: 12px 20px;
+}
+
+.action-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 12px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ transition: all 0.2s;
+ color: #7f8c8d;
+ font-size: 12px;
+ border-radius: 8px;
+ min-width: 60px;
+}
+
+.action-button:hover {
+ background: #f8f9fa;
+ color: #2c3e50;
+}
+
+.action-button.liked {
+ color: #ff4757;
+}
+
+.action-button.bookmarked {
+ color: #ffa726;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .post-detail {
+ margin: 0;
+ }
+
+ .post-title {
+ font-size: 24px;
+ }
+
+ .post-content {
+ padding: 16px;
+ }
+
+ .post-header {
+ padding: 12px 16px;
+ }
+
+ .post-meta {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .post-stats {
+ align-self: flex-end;
+ }
+
+ .action-bar {
+ padding: 8px 16px;
+ }
+
+ .action-button {
+ font-size: 11px;
+ min-width: 50px;
+ }
+}
+
+/* 平滑滚动 */
+html {
+ scroll-behavior: smooth;
+}
+
+/* 选中文本样式 */
+::selection {
+ background: rgba(255, 71, 87, 0.2);
+ color: #2c3e50;
+}
diff --git a/JWLLL/__pycache__/word2vec_helper.cpython-312.pyc b/JWLLL/__pycache__/word2vec_helper.cpython-312.pyc
deleted file mode 100644
index 4036514..0000000
--- a/JWLLL/__pycache__/word2vec_helper.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/JWLLL/main_online.py b/JWLLL/main_online.py
index f8fdc07..d479afa 100644
--- a/JWLLL/main_online.py
+++ b/JWLLL/main_online.py
@@ -339,19 +339,10 @@
# 添加语义关联匹配得分
# 扩展关键词进行匹配
expanded_keywords = expand_search_keywords(keyword)
-
# 检测标题是否包含语义相关词
for exp_keyword in expanded_keywords:
if exp_keyword != keyword and exp_keyword in title: # 避免重复计算原关键词
- # 根据关联词的匹配类型给予不同分数
- if exp_keyword in ["国宝", "熊猫"] and "功夫熊猫" in title:
- score += 3.0 # 高度相关的语义映射
- elif exp_keyword in title:
- score += 1.5 # 一般语义关联
-
- # 对于特殊组合查询,额外加分
- if ("国宝" in keyword or "熊猫" in keyword) and "电影" in keyword and "功夫熊猫" in title:
- score += 4.0 # 对"国宝电影"、"熊猫电影"搜"功夫熊猫"特别加分
+ score += 1.5 # 一般语义关联
return score
@@ -389,6 +380,66 @@
# 在启动应用之前调用初始化函数
initialize_app()
+# 测试路由
+@app.route('/test', methods=['GET'])
+def test():
+ import datetime
+ return jsonify({"message": "服务器正常运行", "timestamp": str(datetime.datetime.now())})
+
+# 获取单个帖子详情的API
+@app.route('/post/<int:post_id>', methods=['GET'])
+def get_post_detail(post_id):
+ """
+ 获取单个帖子详情
+ """
+ logger.info(f"接收到获取帖子详情请求,post_id: {post_id}")
+ conn = get_db_conn()
+ try:
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
+ # 查询帖子详情,先用简单查询调试
+ query = """
+ SELECT
+ p.id,
+ p.title,
+ p.content,
+ p.heat,
+ p.created_at as create_time,
+ p.updated_at as last_active,
+ p.status
+ FROM posts p
+ WHERE p.id = %s
+ """
+ logger.info(f"执行查询: {query} with post_id: {post_id}")
+ cursor.execute(query, (post_id,))
+ post = cursor.fetchone()
+
+ logger.info(f"查询结果: {post}")
+
+ if not post:
+ logger.warning(f"帖子不存在,post_id: {post_id}")
+ return jsonify({"error": "帖子不存在"}), 404
+
+ # 设置默认值
+ post['tags'] = []
+ post['category'] = '未分类'
+ post['author'] = '匿名用户'
+
+ # 格式化时间
+ if post['create_time']:
+ post['create_time'] = post['create_time'].strftime('%Y-%m-%d %H:%M:%S')
+ if post['last_active']:
+ post['last_active'] = post['last_active'].strftime('%Y-%m-%d %H:%M:%S')
+
+ logger.info(f"返回帖子详情: {post}")
+ return Response(json.dumps(post, ensure_ascii=False), mimetype='application/json; charset=utf-8')
+ except Exception as e:
+ logger.error(f"获取帖子详情失败: {e}")
+ import traceback
+ traceback.print_exc()
+ return jsonify({"error": "服务器内部错误"}), 500
+ finally:
+ conn.close()
+
# 搜索功能的API
@app.route('/search', methods=['POST'])
def search():
diff --git a/JWLLL/semantic_config.json b/JWLLL/semantic_config.json
index 9f54454..f5e34d3 100644
--- a/JWLLL/semantic_config.json
+++ b/JWLLL/semantic_config.json
@@ -69,5 +69,9 @@
"技术": ["科技", "工程", "编程", "软件", "硬件", "开发", "技术革新", "IT"],
"监狱": ["越狱", "囚犯", "牢房", "服刑", "狱警"],
- "越狱": ["监狱", "囚犯", "逃狱", "越狱计划", "监狱逃脱"]
+ "越狱": ["监狱", "囚犯", "逃狱", "越狱计划", "监狱逃脱"],
+
+ "肥皂": ["手工皂", "皂"],
+ "手工皂": ["肥皂", "皂"],
+ "皂": ["肥皂", "手工皂"]
}