增加帖子详情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;
+}