上传和查看帖子

Change-Id: I9c23e69d34d24a56e7edd7fb91f5d84bf782834c
diff --git a/API/API-TRM/WZY/xhs_front/src/App.jsx b/API/API-TRM/WZY/xhs_front/src/App.jsx
index 9a79c5c..6af25df 100644
--- a/API/API-TRM/WZY/xhs_front/src/App.jsx
+++ b/API/API-TRM/WZY/xhs_front/src/App.jsx
@@ -3,7 +3,6 @@
 import Header from './components/Header'
 import Sidebar from './components/Sidebar'
 import AppRouter from './router'
-
 import './App.css'
 
 export default function App() {
@@ -20,4 +19,4 @@
       </div>
     </Router>
   )
-}
\ No newline at end of file
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/api/posts.js b/API/API-TRM/WZY/xhs_front/src/api/posts.js
new file mode 100644
index 0000000..89fe7e5
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/api/posts.js
@@ -0,0 +1,129 @@
+// src/api/posts.js
+const BASE = 'http://127.0.0.1:5000/'  // 如果有代理可以留空,否则填完整域名,如 'http://localhost:3000'
+
+/**
+ * 获取所有已发布的帖子列表
+ * GET /posts
+ */
+export async function fetchPosts() {
+  const res = await fetch(`${BASE}/posts`)
+  if (!res.ok) throw new Error(`fetchPosts: ${res.status}`)
+  return res.json()  // 返回 [ { id, title, heat, created_at }, … ]
+}
+
+/**
+ * 查看单个帖子详情
+ * GET /posts/{postId}
+ */
+export async function fetchPost(postId) {
+  const res = await fetch(`${BASE}/posts/${postId}`)
+  if (!res.ok) throw new Error(`fetchPost(${postId}): ${res.status}`)
+  return res.json()  // 返回完整的帖子对象
+}
+
+/**
+ * 发布新帖
+ * POST /posts
+ */
+export async function createPost(payload) {
+  const res = await fetch(`${BASE}/posts`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(payload)
+  })
+  if (!res.ok) {
+    const err = await res.json().catch(() => null)
+    throw new Error(err?.error || `createPost: ${res.status}`)
+  }
+  return res.json()  // { id }
+}
+
+/**
+ * 修改帖子
+ * PUT /posts/{postId}
+ */
+export async function updatePost(postId, payload) {
+  const res = await fetch(`${BASE}/posts/${postId}`, {
+    method: 'PUT',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(payload)
+  })
+  if (!res.ok) throw new Error(`updatePost(${postId}): ${res.status}`)
+  // 204 No Content
+}
+
+/**
+ * 删除帖子
+ * DELETE /posts/{postId}
+ */
+export async function deletePost(postId) {
+  const res = await fetch(`${BASE}/posts/${postId}`, {
+    method: 'DELETE'
+  })
+  if (!res.ok) throw new Error(`deletePost(${postId}): ${res.status}`)
+}
+
+/**
+ * 点赞
+ * POST /posts/{postId}/like
+ */
+export async function likePost(postId, userId) {
+  const res = await fetch(`${BASE}/posts/${postId}/like`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ user_id: userId })
+  })
+  if (!res.ok) {
+    const err = await res.json().catch(() => null)
+    throw new Error(err?.error || `likePost: ${res.status}`)
+  }
+}
+
+/**
+ * 取消点赞
+ * DELETE /posts/{postId}/like
+ */
+export async function unlikePost(postId, userId) {
+  const res = await fetch(`${BASE}/posts/${postId}/like`, {
+    method: 'DELETE',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ user_id: userId })
+  })
+  if (!res.ok) {
+    const err = await res.json().catch(() => null)
+    throw new Error(err?.error || `unlikePost: ${res.status}`)
+  }
+}
+
+/**
+ * 收藏、取消收藏、浏览、分享 等接口:
+ * POST   /posts/{postId}/favorite
+ * DELETE /posts/{postId}/favorite
+ * POST   /posts/{postId}/view
+ * POST   /posts/{postId}/share
+ * 用法同上,替换路径即可
+ */
+
+/**
+ * 添加评论
+ * POST /posts/{postId}/comments
+ */
+export async function addComment(postId, payload) {
+  const res = await fetch(`${BASE}/posts/${postId}/comments`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(payload)
+  })
+  if (!res.ok) throw new Error(`addComment: ${res.status}`)
+  return res.json()  // { id }
+}
+
+/**
+ * 获取评论列表
+ * GET /posts/{postId}/comments
+ */
+export async function fetchComments(postId) {
+  const res = await fetch(`${BASE}/posts/${postId}/comments`)
+  if (!res.ok) throw new Error(`fetchComments: ${res.status}`)
+  return res.json()
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/components/CreatePost.jsx b/API/API-TRM/WZY/xhs_front/src/components/CreatePost.jsx
new file mode 100644
index 0000000..51635c2
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/CreatePost.jsx
@@ -0,0 +1,168 @@
+// src/components/CreatePost.jsx
+
+import React, { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import UploadPage from './UploadPage'
+import { createPost } from '../api/posts'
+import '../style/CreatePost.css'
+
+export default function CreatePost() {
+  const navigate = useNavigate()
+
+  const [step, setStep] = useState('upload')     // 'upload' | 'detail'
+  const [files, setFiles] = useState([])         // 本地 File 对象列表
+  const [mediaUrls, setMediaUrls] = useState([]) // 上传后得到的 URL 列表
+
+  // 详情表单字段
+  const [title, setTitle]     = useState('')
+  const [content, setContent] = useState('')
+  const [topicId, setTopicId] = useState('')
+  const [status, setStatus]   = useState('published')
+
+  const [error, setError]     = useState(null)
+
+  // 静态话题数据
+  const TOPICS = [
+    { id: 1, name: '世俱杯环球评大会' },
+    { id: 2, name: '我的REDmentor' },
+    { id: 3, name: '我染上了拼豆' },
+    // …更多静态话题…
+  ]
+
+  // 上传页面回调 —— 上传完成后切换到“填写详情”步骤
+  const handleUploadComplete = async uploadedFiles => {
+    setFiles(uploadedFiles)
+
+    // TODO: 改成真实上传逻辑,拿到真正的 media_urls
+    const urls = await Promise.all(
+      uploadedFiles.map(f => URL.createObjectURL(f))
+    )
+    setMediaUrls(urls)
+
+    setStep('detail')
+  }
+
+  // 发布按钮
+  const handleSubmit = async () => {
+    if (!title.trim() || !content.trim()) {
+      setError('标题和正文必填')
+      return
+    }
+    setError(null)
+    try {
+      await createPost({
+        user_id: 1,
+        topic_id: topicId || undefined,
+        title: title.trim(),
+        content: content.trim(),
+        media_urls: mediaUrls,
+        status
+      })
+      // 发布成功后跳转回首页
+      navigate('/home', { replace: true })
+    } catch (e) {
+      setError(e.message)
+    }
+  }
+
+  // 渲染上传页
+  if (step === 'upload') {
+    return <UploadPage onComplete={handleUploadComplete} />
+  }
+
+  // 渲染详情页
+  return (
+    <div className="create-post">
+      <h2>填写帖子内容</h2>
+      {error && <div className="error">{error}</div>}
+
+      {/* 已上传媒体预览 */}
+      <div className="preview-media">
+        {mediaUrls.map((url, i) => (
+          <div key={i} className="preview-item">
+            {files[i].type.startsWith('image/') ? (
+              <img src={url} alt={`预览 ${i}`} />
+            ) : (
+              <video src={url} controls />
+            )}
+          </div>
+        ))}
+      </div>
+
+      {/* 标题 */}
+      <label className="form-label">
+        标题(最多20字)
+        <input
+          type="text"
+          maxLength={20}
+          value={title}
+          onChange={e => setTitle(e.target.value)}
+          placeholder="填写标题会有更多赞哦~"
+        />
+        <span className="char-count">{title.length}/20</span>
+      </label>
+
+      {/* 正文 */}
+      <label className="form-label">
+        正文(最多1000字)
+        <textarea
+          maxLength={1000}
+          value={content}
+          onChange={e => setContent(e.target.value)}
+          placeholder="输入正文描述,真诚有价值的分享予人温暖"
+        />
+        <span className="char-count">{content.length}/1000</span>
+      </label>
+
+      {/* 话题选择 */}
+      <label className="form-label">
+        选择话题(可选)
+        <select
+          value={topicId}
+          onChange={e => setTopicId(e.target.value)}
+        >
+          <option value="">不添加话题</option>
+          {TOPICS.map(t => (
+            <option key={t.id} value={t.id}>
+              #{t.name}
+            </option>
+          ))}
+        </select>
+      </label>
+
+      {/* 发布状态 */}
+      <div className="status-group">
+        <label>
+          <input
+            type="radio"
+            name="status"
+            value="published"
+            checked={status === 'published'}
+            onChange={() => setStatus('published')}
+          />
+          立即发布
+        </label>
+        <label>
+          <input
+            type="radio"
+            name="status"
+            value="draft"
+            checked={status === 'draft'}
+            onChange={() => setStatus('draft')}
+          />
+          存为草稿
+        </label>
+      </div>
+
+      {/* 操作按钮 */}
+      <div className="btn-group">
+        <button className="btn btn-primary" onClick={handleSubmit}>
+          发布
+        </button>
+        <button className="btn btn-secondary" onClick={() => setStep('upload')}>
+          上一步
+        </button>
+      </div>
+    </div>
+  )
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx b/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx
index a7f70b5..d906e33 100644
--- a/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx
+++ b/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx
@@ -1,15 +1,9 @@
-import React, { useState } from 'react'
-import { ThumbsUp } from 'lucide-react'
-import '../style/HomeFeed.css'
+// src/components/HomeFeed.jsx
 
-const mockItems = Array.from({ length: 20 }).map((_, i) => ({
-  id: i,
-  title: `示例标题 ${i + 1}`,
-  author: `作者 ${i + 1}`,
-  avatar: `https://i.pravatar.cc/40?img=${i + 1}`,
-  img: `https://picsum.photos/seed/${i}/300/200`,
-  likes: Math.floor(Math.random() * 1000)
-}))
+import React, { useState, useEffect } from 'react'
+import { ThumbsUp } from 'lucide-react'
+import { fetchPosts, fetchPost } from '../api/posts'
+import '../style/HomeFeed.css'
 
 const categories = [
   '推荐','穿搭','美食','彩妆','影视',
@@ -18,7 +12,37 @@
 
 export default function HomeFeed() {
   const [activeCat, setActiveCat] = useState('推荐')
-  const items = mockItems.filter(() => true) // 可按 activeCat 过滤
+  const [items, setItems]         = useState([])
+  const [loading, setLoading]     = useState(true)
+  const [error, setError]         = useState(null)
+
+  useEffect(() => {
+    async function loadPosts() {
+      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)
+      }
+    }
+    loadPosts()
+  }, [])
 
   return (
     <div className="home-feed">
@@ -35,38 +59,32 @@
         ))}
       </nav>
 
-      {/* 瀑布流卡片区 */}
-      <div className="feed-grid">
-        {items.map(item => (
-          <div key={item.id} className="feed-card">
-            {/* 封面图 */}
-            <img
-              className="card-img"
-              src={item.img}
-              alt={item.title}
-            />
-
-            {/* 标题 */}
-            <h3 className="card-title">{item.title}</h3>
-
-            {/* 底部作者 + 点赞 */}
-            <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>
+      {/* 状态提示 */}
+      {loading ? (
+        <div className="loading">加载中…</div>
+      ) : error ? (
+        <div className="error">加载失败:{error}</div>
+      ) : (
+        /* 瀑布流卡片区 */
+        <div className="feed-grid">
+          {items.map(item => (
+            <div key={item.id} className="feed-card">
+              <img className="card-img" src={item.img} alt={item.title} />
+              <h3 className="card-title">{item.title}</h3>
+              <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>
+          ))}
+        </div>
+      )}
     </div>
   )
-}
\ No newline at end of file
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx b/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx
index b6acad5..26118b2 100644
--- a/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx
+++ b/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx
@@ -8,7 +8,7 @@
   Users,
   ChevronDown,
 } from 'lucide-react'
-
+import '../App.css'
 
 const menuItems = [
   { id: 'home',      label: '首页',       icon: Home,      path: '/home' },
@@ -35,6 +35,7 @@
   const location = useLocation()
   const navigate = useNavigate()
 
+  // 打开 dashboard 下拉时保持展开
   useEffect(() => {
     if (location.pathname.startsWith('/dashboard')) {
       setExpandedMenu('dashboard')
@@ -52,7 +53,14 @@
 
   return (
     <aside className="sidebar">
-      <button className="publish-btn">发布笔记</button>
+      {/* 发布笔记 按钮 */}
+      <button
+        className="publish-btn"
+        onClick={() => navigate('/posts/new')}
+      >
+        发布笔记
+      </button>
+
       <nav className="nav-menu">
         {menuItems.map(item => (
           <div key={item.id} className="nav-item">
@@ -92,4 +100,4 @@
       </nav>
     </aside>
   )
-}
\ No newline at end of file
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx b/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx
index e6db861..dbbd7fe 100644
--- a/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx
+++ b/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx
@@ -1,8 +1,14 @@
+// src/components/UploadPage.jsx
+
 import React, { useState } from 'react'
 import { Image, Video } from 'lucide-react'
 
 
-export default function UploadPage() {
+/**
+ * @param {Object} props
+ * @param {(files: File[]) => void} [props.onComplete]  上传完成后回调,接收 File 数组
+ */
+export default function UploadPage({ onComplete }) {
   const [activeTab, setActiveTab]         = useState('image')
   const [isDragOver, setIsDragOver]       = useState(false)
   const [isUploading, setIsUploading]     = useState(false)
@@ -12,8 +18,10 @@
   const validateFiles = files => {
     const imgTypes = ['image/jpeg','image/jpg','image/png','image/webp']
     const vidTypes = ['video/mp4','video/mov','video/avi']
-    const types = activeTab==='video'? vidTypes : imgTypes
-    const max   = activeTab==='video'? 2*1024*1024*1024 : 32*1024*1024
+    const types = activeTab === 'video' ? vidTypes : imgTypes
+    const max   = activeTab === 'video'
+      ? 2 * 1024 * 1024 * 1024
+      : 32 * 1024 * 1024
 
     const invalid = files.filter(f => !types.includes(f.type) || f.size > max)
     if (invalid.length) {
@@ -27,12 +35,17 @@
     setIsUploading(true)
     setUploadProgress(0)
     setUploadedFiles(files)
+
     const iv = setInterval(() => {
       setUploadProgress(p => {
         if (p >= 100) {
           clearInterval(iv)
           setIsUploading(false)
           alert(`成功上传了 ${files.length} 个文件`)
+          // 上传完成后回调
+          if (typeof onComplete === 'function') {
+            onComplete(files)
+          }
           return 100
         }
         return p + 10
@@ -43,12 +56,14 @@
   const handleFileUpload = () => {
     if (isUploading) return
     const input = document.createElement('input')
-    input.type = 'file'
-    input.accept = activeTab==='video'? 'video/*' : 'image/*'
-    input.multiple = activeTab==='image'
+    input.type     = 'file'
+    input.accept   = activeTab === 'video' ? 'video/*' : 'image/*'
+    input.multiple = activeTab === 'image'
     input.onchange = e => {
       const files = Array.from(e.target.files)
-      if (files.length > 0 && validateFiles(files)) simulateUpload(files)
+      if (files.length > 0 && validateFiles(files)) {
+        simulateUpload(files)
+      }
     }
     input.click()
   }
@@ -59,52 +74,58 @@
     e.preventDefault(); e.stopPropagation(); setIsDragOver(false)
     if (isUploading) return
     const files = Array.from(e.dataTransfer.files)
-    if (files.length > 0 && validateFiles(files)) simulateUpload(files)
+    if (files.length > 0 && validateFiles(files)) {
+      simulateUpload(files)
+    }
   }
 
   const clearFiles = () => setUploadedFiles([])
-  const removeFile = idx => setUploadedFiles(f => f.filter((_,i) => i!==idx))
+  const removeFile = idx => setUploadedFiles(prev => prev.filter((_, i) => i !== idx))
 
   return (
-    <>
+    <div className="upload-page">
+      {/* 上传类型切换 */}
       <div className="upload-tabs">
         <button
-          className={`upload-tab${activeTab==='video'?' active':''}`}
+          className={`upload-tab${activeTab === 'video' ? ' active' : ''}`}
           onClick={() => setActiveTab('video')}
-        >上传视频</button>
+        >
+          上传视频
+        </button>
         <button
-          className={`upload-tab${activeTab==='image'?' active':''}`}
+          className={`upload-tab${activeTab === 'image' ? ' active' : ''}`}
           onClick={() => setActiveTab('image')}
-        >上传图文</button>
+        >
+          上传图文
+        </button>
       </div>
 
+      {/* 拖拽/点击上传区域 */}
       <div
-        className={`upload-area${isDragOver?' drag-over':''}`}
+        className={`upload-area${isDragOver ? ' drag-over' : ''}`}
         onDragOver={handleDragOver}
         onDragLeave={handleDragLeave}
         onDrop={handleDrop}
       >
         <div className="upload-icon">
-          {activeTab==='video'? <Video/> : <Image/>}
+          {activeTab === 'video' ? <Video size={48} /> : <Image size={48} />}
         </div>
         <h2 className="upload-title">
-          {activeTab==='video'
+          {activeTab === 'video'
             ? '拖拽视频到此处或点击上传'
-            : '拖拽图片到此处或点击上传'
-          }
+            : '拖拽图片到此处或点击上传'}
         </h2>
         <p className="upload-subtitle">(需支持上传格式)</p>
         <button
-          className={`upload-btn${isUploading?' uploading':''}`}
+          className={`upload-btn${isUploading ? ' uploading' : ''}`}
           onClick={handleFileUpload}
           disabled={isUploading}
         >
           {isUploading
             ? `上传中... ${uploadProgress}%`
-            : activeTab==='video'
+            : activeTab === 'video'
               ? '上传视频'
-              : '上传图片'
-          }
+              : '上传图片'}
         </button>
 
         {isUploading && (
@@ -120,11 +141,17 @@
         )}
       </div>
 
+      {/* 已上传文件预览 */}
       {uploadedFiles.length > 0 && (
         <div className="file-preview-area">
           <div className="preview-header">
-            <h3 className="preview-title">已上传文件 ({uploadedFiles.length})</h3>
-            <button className="clear-files-btn" onClick={clearFiles}>
+            <h3 className="preview-title">
+              已上传文件 ({uploadedFiles.length})
+            </h3>
+            <button
+              className="clear-files-btn"
+              onClick={clearFiles}
+            >
               清除所有
             </button>
           </div>
@@ -135,7 +162,9 @@
                   className="remove-file-btn"
                   onClick={() => removeFile(i)}
                   title="删除文件"
-                >×</button>
+                >
+                  ×
+                </button>
                 {file.type.startsWith('image/') ? (
                   <div className="file-thumbnail">
                     <img src={URL.createObjectURL(file)} alt={file.name} />
@@ -148,12 +177,11 @@
                 <div className="file-info">
                   <div className="file-name" title={file.name}>
                     {file.name.length > 20
-                      ? file.name.slice(0,17) + '...'
-                      : file.name
-                    }
+                      ? file.name.slice(0, 17) + '...'
+                      : file.name}
                   </div>
                   <div className="file-size">
-                    {(file.size/1024/1024).toFixed(2)} MB
+                    {(file.size / 1024 / 1024).toFixed(2)} MB
                   </div>
                 </div>
               </div>
@@ -162,8 +190,9 @@
         </div>
       )}
 
+      {/* 上传说明信息 */}
       <div className="upload-info fade-in">
-        {activeTab==='image' ? (
+        {activeTab === 'image' ? (
           <>
             <div className="info-item">
               <h3 className="info-title">图片大小</h3>
@@ -195,6 +224,6 @@
           </>
         )}
       </div>
-    </>
+    </div>
   )
-}
\ No newline at end of file
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/router/index.jsx b/API/API-TRM/WZY/xhs_front/src/router/index.jsx
index 64191ac..077f8ed 100644
--- a/API/API-TRM/WZY/xhs_front/src/router/index.jsx
+++ b/API/API-TRM/WZY/xhs_front/src/router/index.jsx
@@ -1,24 +1,34 @@
+// src/router/index.jsx
 import React from 'react'
 import { Routes, Route, Navigate } from 'react-router-dom'
-import UploadPage from '../components/UploadPage'
-import PlaceholderPage from '../components/PlaceholderPage'
-import HomeFeed from '../components/HomeFeed'
+
+// 确认这些路径和文件名与你项目里是一一对应的
+import CreatePost     from '../components/CreatePost'      // src/components/CreatePost.jsx
+import HomeFeed       from '../components/HomeFeed'        // src/components/HomeFeed.jsx
+import PlaceholderPage from '../components/PlaceholderPage'// src/components/PlaceholderPage.jsx
+import UploadPage     from '../components/UploadPage'      // src/components/UploadPage.jsx
 
 export default function AppRouter() {
   return (
     <Routes>
-      <Route path="/" element={<Navigate to="/dashboard" replace />} />
+      {/* 一定要放在最前面,防止被 /* 等 catch-all 覆盖 */}
+      <Route path="/posts/new" element={<CreatePost />} />
 
       <Route path="/home"      element={<HomeFeed />} />
+
       <Route path="/notebooks" element={<PlaceholderPage pageId="notebooks" />} />
-      <Route path="/activity"  element={<PlaceholderPage pageId="activity" />} />
-      <Route path="/notes"     element={<PlaceholderPage pageId="notes" />} />
-      <Route path="/creator"   element={<PlaceholderPage pageId="creator" />} />
-      <Route path="/journal"   element={<PlaceholderPage pageId="journal" />} />
+      <Route path="/activity"  element={<PlaceholderPage pageId="activity"  />} />
+      <Route path="/notes"     element={<PlaceholderPage pageId="notes"     />} />
+      <Route path="/creator"   element={<PlaceholderPage pageId="creator"   />} />
+      <Route path="/journal"   element={<PlaceholderPage pageId="journal"   />} />
 
       <Route path="/dashboard/*" element={<UploadPage />} />
 
+      {/* 根路径重定向到 dashboard */}
+      <Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
+
+      {/* 最后一个兜底 */}
       <Route path="*" element={<PlaceholderPage pageId="home" />} />
     </Routes>
   )
-}
\ No newline at end of file
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/style/CreatePost.css b/API/API-TRM/WZY/xhs_front/src/style/CreatePost.css
new file mode 100644
index 0000000..4868132
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/style/CreatePost.css
@@ -0,0 +1,98 @@
+/* src/style/CreatePost.css */
+.create-post {
+  max-width: 600px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+}
+
+/* 预览区 */
+.preview-media {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-bottom: 20px;
+}
+.preview-item {
+  width: 100px;
+  height: 100px;
+  overflow: hidden;
+  border: 1px solid #eee;
+  border-radius: 4px;
+}
+.preview-item img,
+.preview-item video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+/* 表单项 */
+label {
+  display: block;
+  margin-bottom: 16px;
+  font-size: 14px;
+  color: #333;
+}
+label input[type="text"],
+label textarea,
+label select {
+  width: 100%;
+  padding: 8px;
+  margin-top: 6px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-size: 14px;
+  box-sizing: border-box;
+}
+label textarea {
+  min-height: 120px;
+  resize: vertical;
+}
+.char-count {
+  float: right;
+  font-size: 12px;
+  color: #999;
+}
+
+/* 发布状态 */
+.status-group {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 20px;
+}
+.status-group label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 14px;
+}
+
+/* 按钮组 */
+.btn-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+.btn {
+  padding: 8px 16px;
+  border-radius: 4px;
+  border: none;
+  cursor: pointer;
+  font-size: 14px;
+}
+.btn-primary {
+  background: #ff4757;
+  color: #fff;
+}
+.btn-secondary {
+  background: #f0f0f0;
+  color: #333;
+}
+
+/* 错误信息 */
+.error {
+  color: #d9534f;
+  margin-bottom: 12px;
+}
diff --git a/API/API-TRM/WZY/xhs_server/app.py b/API/API-TRM/WZY/xhs_server/app.py
index 226bf18..649a7a5 100644
--- a/API/API-TRM/WZY/xhs_server/app.py
+++ b/API/API-TRM/WZY/xhs_server/app.py
@@ -1,25 +1,34 @@
 # app.py
+
 from flask import Flask
-from config     import Config
-from extensions import db, migrate  # ← 改自 extensions
+from flask_cors import CORS
+from config import Config
+from extensions import db, migrate
 
 def create_app():
     app = Flask(__name__)
     app.config.from_object(Config)
 
+    # 启用 CORS:允许前端 http://localhost:5173 发起跨域请求
+    # 生产环境请根据实际域名调整 origins
+    CORS(app, resources={
+        r"/posts/*": {"origins": "http://localhost:5173"},
+        r"/posts":   {"origins": "http://localhost:5173"}
+    }, supports_credentials=True)
+
     db.init_app(app)
     migrate.init_app(app, db)
 
-    # 在工厂函数里再导入、注册蓝图
+    # 在工厂函数里再导入并注册蓝图
     from routes.posts    import posts_bp
     from routes.comments import comments_bp
 
     app.register_blueprint(posts_bp,    url_prefix='/posts')
     app.register_blueprint(comments_bp, url_prefix='/posts/<int:post_id>/comments')
+
     return app
 
-app = create_app()
-
+# 只有直接用 python app.py 时,这段才会执行
 if __name__ == '__main__':
-    # 只有直接用 python app.py 时,这段才会执行
-    app.run(host='0.0.0.0', port=5000, debug=True)
\ No newline at end of file
+    app = create_app()
+    app.run(host='0.0.0.0', port=5000, debug=True)
diff --git a/API/API-TRM/WZY/xhs_server/readme.md b/API/API-TRM/WZY/xhs_server/readme.md
new file mode 100644
index 0000000..9fab23e
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/readme.md
@@ -0,0 +1,400 @@
+## 2. 帖子(Post)
+
+### 2.1 发布新帖
+
+```
+POST /posts
+```
+
+- **描述**:创建一条帖子
+
+- **请求头**
+
+  ```
+  Content-Type: application/json
+  ```
+
+- **请求体**
+
+  ```json
+  {
+    "user_id": 1,
+    "topic_id": 1,                    // 可选,必须是已存在 topic 的 ID
+    "title": "帖子标题",
+    "content": "正文内容",
+    "media_urls": [                   // 可选,字符串数组
+      "https://example.com/img1.jpg",
+      "https://example.com/vid1.mp4"
+    ],
+    "status": "published"             // draft|pending|published|deleted|rejected
+  }
+  ```
+
+- **成功响应**
+
+  - **状态**:201 Created
+
+  - **Body**
+
+    ```json
+    { "id": 42 }
+    ```
+
+- **错误**
+
+  - 400 Bad Request: 缺少 user_id、title 或 content
+  - 400 Bad Request: topic_id 不存在
+  - 400 Bad Request: JSON 解析错误
+  - 422 Unprocessable Entity: media_urls 格式错误
+  - 500 Internal Server Error: 外键约束或其他数据库错误
+
+------
+
+### 2.2 获取帖子列表
+
+```
+GET /posts
+```
+
+- **描述**:拉取所有 `status=published` 的帖子
+
+- **响应**
+
+  - **状态**:200 OK
+
+  - **Body**
+
+    ```json
+    [
+      {
+        "id": 42,
+        "title": "帖子标题",
+        "heat": 5,
+        "created_at": "2025-06-12T16:00:00"
+      },
+      ...
+    ]
+    ```
+
+------
+
+### 2.3 查看帖子详情
+
+```
+GET /posts/{post_id}
+```
+
+- **描述**:查看单条帖子完整信息
+
+- **路径参数**
+
+  | 参数    | 描述    |
+  | ------- | ------- |
+  | post_id | 帖子 ID |
+
+- **响应**
+
+  - **状态**:200 OK
+
+  - **Body**
+
+    ```json
+    {
+      "id": 42,
+      "user_id": 1,
+      "topic_id": 1,
+      "title": "帖子标题",
+      "content": "正文内容",
+      "media_urls": ["…"],
+      "status": "published",
+      "heat": 5,
+      "created_at": "2025-06-12T16:00:00",
+      "updated_at": "2025-06-12T16:05:00"
+    }
+    ```
+
+- **错误**
+
+  - 404 Not Found: 帖子不存在
+
+------
+
+### 2.4 修改帖子
+
+```
+PUT /posts/{post_id}
+```
+
+- **描述**:更新帖子字段
+
+- **路径参数**
+
+  | 参数    | 描述    |
+  | ------- | ------- |
+  | post_id | 帖子 ID |
+
+- **请求头**
+
+  ```
+  Content-Type: application/json
+  ```
+
+- **请求体**(所有字段可选,依需更新)
+
+  ```json
+  {
+    "title": "新标题",
+    "content": "新内容",
+    "topic_id": 2,
+    "media_urls": ["…"],
+    "status": "draft"
+  }
+  ```
+
+- **响应**
+
+  - **状态**:204 No Content
+
+- **错误**
+
+  - 400 Bad Request: JSON 格式或字段值不合法
+  - 404 Not Found: 帖子不存在
+
+------
+
+### 2.5 删除帖子
+
+```
+DELETE /posts/{post_id}
+```
+
+- **描述**:删除帖子及其关联行为、评论
+
+- **路径参数**
+
+  | 参数    | 描述    |
+  | ------- | ------- |
+  | post_id | 帖子 ID |
+
+- **响应**
+
+  - **状态**:204 No Content
+
+- **错误**
+
+  - 404 Not Found: 帖子不存在
+
+------
+
+## 3. 互动行为(Behavior)
+
+> 支持四种操作:`like`、`favorite`、`view`、`share`。其中 `like` 和 `favorite` 限制每人每帖最多一次,可撤销;`view`/`share` 不限次数,不提供撤销。
+
+### 3.1 点赞
+
+```
+POST /posts/{post_id}/like
+```
+
+- **描述**:用户点赞,热度 +1
+
+- **请求头**
+
+  ```
+  Content-Type: application/json
+  ```
+
+- **请求体**
+
+  ```json
+  { "user_id": 1 }
+  ```
+
+- **响应**
+
+  - **状态**:201 Created
+
+- **错误**
+
+  - 400 Bad Request: 已点赞过 → `{"error":"already liked"}`
+  - 400 Bad Request: 缺少 user_id
+  - 404 Not Found: 帖子或用户不存在
+
+### 3.2 取消点赞
+
+```
+DELETE /posts/{post_id}/like
+```
+
+- **描述**:撤销点赞,热度 -1(底线 0)
+
+- **请求头**
+
+  ```
+  Content-Type: application/json
+  ```
+
+- **请求体**
+
+  ```json
+  { "user_id": 1 }
+  ```
+
+- **响应**
+
+  - **状态**:204 No Content
+
+- **错误**
+
+  - 400 Bad Request: 未点赞过 → `{"error":"not liked yet"}`
+  - 404 Not Found: 帖子或用户不存在
+
+------
+
+### 3.3 收藏
+
+```
+POST /posts/{post_id}/favorite
+```
+
+- **描述**:用户收藏,热度 +1
+- **请求头/体/响应/错误**
+   与点赞接口完全一致,只把 `like` 换成 `favorite`,错误信息为 `already favorited` / `not favorited yet`。
+
+### 3.4 取消收藏
+
+```
+DELETE /posts/{post_id}/favorite
+```
+
+- **描述**:撤销收藏,热度 -1
+- **请求头/体/响应/错误**
+   同上。
+
+------
+
+### 3.5 浏览
+
+```
+POST /posts/{post_id}/view
+```
+
+- **描述**:记录一次浏览,热度 +1
+
+- **请求体**
+
+  ```json
+  { "user_id": 1 }
+  ```
+
+- **响应**
+
+  - 201 Created
+
+不支持撤销;不做去重检查。
+
+------
+
+### 3.6 分享
+
+```
+POST /posts/{post_id}/share
+```
+
+- **描述**:记录一次分享,热度 +1
+- **请求体/响应**
+   同浏览。
+
+------
+
+## 4. 评论(Comment)
+
+### 4.1 添加评论
+
+```
+POST /posts/{post_id}/comments
+```
+
+- **描述**:为帖子添加评论或回复
+
+- **请求头**
+
+  ```
+  Content-Type: application/json
+  ```
+
+- **请求体**
+
+  ```json
+  {
+    "user_id": 2,
+    "content": "这是评论内容",
+    "parent_id": 1    // 可选:回复某条评论时填,一级评论则省略
+  }
+  ```
+
+- **响应**
+
+  - **状态**:201 Created
+
+  - **Body**
+
+    ```json
+    { "id": 7 }
+    ```
+
+- **错误**
+
+  - 400 Bad Request: 缺少 user_id 或 content
+  - 404 Not Found: 帖子或 parent_id 不存在
+
+------
+
+### 4.2 获取评论列表
+
+```
+GET /posts/{post_id}/comments
+```
+
+- **描述**:拉取该帖所有一级评论及其完整回复树
+
+- **响应**
+
+  - **状态**:200 OK
+
+  - **Body**
+
+    ```json
+    [
+      {
+        "id": 1,
+        "user_id": 1,
+        "content": "一级评论",
+        "created_at": "…",
+        "replies": [
+          {
+            "id": 2,
+            "user_id": 2,
+            "content": "回复评论",
+            "created_at": "…",
+            "replies": [ … ]
+          }
+        ]
+      },
+      …
+    ]
+    ```
+
+- **错误**
+
+  - 404 Not Found: 帖子不存在
+
+------
+
+> **通用错误响应格式**
+>
+> ```json
+> {
+>   "error": "描述信息"
+> }
+> ```
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc
index 119a503..a957747 100644
--- a/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc
+++ b/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc
Binary files differ