新增管理帖子的组件

Change-Id: I7299ad6f735064dd112f92bab7c87daa952e6dc2
diff --git a/Merge/front/src/components/CreatePost.jsx b/Merge/front/src/components/CreatePost.jsx
index 7519d5b..9817ac0 100644
--- a/Merge/front/src/components/CreatePost.jsx
+++ b/Merge/front/src/components/CreatePost.jsx
@@ -1,48 +1,68 @@
 // src/components/CreatePost.jsx
 
-import React, { useState } from 'react'
-import { useNavigate } from 'react-router-dom'
+import React, { useState, useEffect } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
 import UploadPage from './UploadPage'
-import { createPost } from '../api/posts_wzy'
+import { 
+  createPost, 
+  updatePost, 
+  fetchPost as fetchPostDetail 
+} from '../api/posts_wzy'
 import '../style/CreatePost.css'
 
 export default function CreatePost() {
   const navigate = useNavigate()
+  const { postId } = useParams()
+  const isEdit = Boolean(postId)
 
-  const [step, setStep] = useState('upload')     // 'upload' | 'detail'
-  const [files, setFiles] = useState([])         // 本地 File 对象列表
-  const [mediaUrls, setMediaUrls] = useState([]) // 上传后得到的 URL 列表
+  // 步骤:新帖先上传,编辑则直接到 detail
+  const [step, setStep] = useState(isEdit ? 'detail' : 'upload')
+  const [files, setFiles] = useState([])
+  const [mediaUrls, setMediaUrls] = useState([])
 
-  // 详情表单字段
+  // 表单字段
   const [title, setTitle]     = useState('')
   const [content, setContent] = useState('')
   const [topicId, setTopicId] = useState('')
   const [status, setStatus]   = useState('published')
 
   const [error, setError]     = useState(null)
+  const [loading, setLoading] = useState(isEdit)
 
-  // 静态话题数据
+  // 静态话题
   const TOPICS = [
     { id: 1, name: '世俱杯环球评大会' },
     { id: 2, name: '我的REDmentor' },
     { id: 3, name: '我染上了拼豆' },
-    // …更多静态话题…
   ]
 
-  // 上传页面回调 —— 上传完成后切换到“填写详情”步骤
+  // 编辑模式:拉取原帖数据填入
+  useEffect(() => {
+    if (!isEdit) return
+    fetchPostDetail(postId)
+      .then(data => {
+        setTitle(data.title)
+        setContent(data.content)
+        setTopicId(data.topic_id || '')
+        setStatus(data.status)
+        setMediaUrls(data.media_urls || [])
+      })
+      .catch(err => setError(err.message))
+      .finally(() => setLoading(false))
+  }, [isEdit, postId])
+
+  // 上传回调
   const handleUploadComplete = async uploadedFiles => {
     setFiles(uploadedFiles)
-
-    // TODO: 改成真实上传逻辑,拿到真正的 media_urls
+    // TODO: 真正上传到服务器后替换为服务端 URL
     const urls = await Promise.all(
       uploadedFiles.map(f => URL.createObjectURL(f))
     )
     setMediaUrls(urls)
-
     setStep('detail')
   }
 
-  // 发布按钮
+  // 提交(创建/更新)
   const handleSubmit = async () => {
     if (!title.trim() || !content.trim()) {
       setError('标题和正文必填')
@@ -50,40 +70,50 @@
     }
     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 })
+      if (isEdit) {
+        await updatePost(postId, {
+          title: title.trim(),
+          content: content.trim(),
+          topic_id: topicId || undefined,
+          media_urls: mediaUrls,
+          status
+        })
+        alert('更新成功!')
+      } else {
+        await createPost({
+          user_id: 1,
+          topic_id: topicId || undefined,
+          title: title.trim(),
+          content: content.trim(),
+          media_urls: mediaUrls,
+          status
+        })
+        alert('发布成功!')
+      }
+      navigate('/notebooks', { replace: true })
     } catch (e) {
       setError(e.message)
     }
   }
 
-  // 渲染上传页
-  if (step === 'upload') {
+  if (loading) return <p>加载中…</p>
+  if (step === 'upload' && !isEdit) {
     return <UploadPage onComplete={handleUploadComplete} />
   }
 
-  // 渲染详情页
   return (
     <div className="create-post">
-      <h2>填写帖子内容</h2>
+      <h2>{isEdit ? '编辑帖子' : '填写帖子内容'}</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}`} />
-            ) : (
+            {url.match(/\.(mp4|mov|avi)$/) ? (
               <video src={url} controls />
+            ) : (
+              <img src={url} alt={`预览 ${i}`} />
             )}
           </div>
         ))}
@@ -97,7 +127,6 @@
           maxLength={20}
           value={title}
           onChange={e => setTitle(e.target.value)}
-          placeholder="填写标题会有更多赞哦~"
         />
         <span className="char-count">{title.length}/20</span>
       </label>
@@ -109,7 +138,6 @@
           maxLength={1000}
           value={content}
           onChange={e => setContent(e.target.value)}
-          placeholder="输入正文描述,真诚有价值的分享予人温暖"
         />
         <span className="char-count">{content.length}/1000</span>
       </label>
@@ -154,14 +182,16 @@
         </label>
       </div>
 
-      {/* 操作按钮 */}
+      {/* 按钮 */}
       <div className="btn-group">
         <button className="btn btn-primary" onClick={handleSubmit}>
-          发布
+          {isEdit ? '更新' : '发布'}
         </button>
-        <button className="btn btn-secondary" onClick={() => setStep('upload')}>
-          上一步
-        </button>
+        {!isEdit && (
+          <button className="btn btn-secondary" onClick={() => setStep('upload')}>
+            上一步
+          </button>
+        )}
       </div>
     </div>
   )
diff --git a/Merge/front/src/components/NotebookPage.jsx b/Merge/front/src/components/NotebookPage.jsx
new file mode 100644
index 0000000..25264ec
--- /dev/null
+++ b/Merge/front/src/components/NotebookPage.jsx
@@ -0,0 +1,85 @@
+// src/components/NotebookPage.jsx
+
+import React, { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { fetchPosts, deletePost } from '../api/posts_wzy'
+import '../style/NotebookPage.css'
+
+export default function NotebookPage() {
+  const navigate = useNavigate()
+  const [posts, setPosts]     = useState([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError]     = useState(null)
+
+  // TODO: 替换成真实用户 ID
+  const currentUserId = 2
+
+  useEffect(() => {
+    async function load() {
+      try {
+        // GET /posts?user_id=1
+        const list = await fetchPosts(currentUserId)
+        setPosts(list)
+      } catch (e) {
+        setError(e.message)
+      } finally {
+        setLoading(false)
+      }
+    }
+    load()
+  }, [])
+
+  async function handleDelete(id) {
+    if (!window.confirm('确定要删除该帖子吗?')) return
+    try {
+      await deletePost(id)
+      setPosts(posts.filter(p => p.id !== id))
+      alert('删除成功')
+    } catch (e) {
+      alert('删除失败:' + e.message)
+    }
+  }
+
+  function handleEdit(id) {
+    // 假设你在路由里挂载了 /posts/edit/:postId
+    navigate(`/posts/edit/${id}`)
+  }
+
+  if (loading) return <p>加载中…</p>
+  if (error)   return <p className="error">加载失败:{error}</p>
+
+  return (
+    <div className="notebook-page">
+      <h2>我的帖子管理</h2>
+      {posts.length === 0 ? (
+        <p>暂无帖子</p>
+      ) : (
+        <table className="post-table">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>标题</th>
+              <th>状态</th>
+              <th>创建时间</th>
+              <th>操作</th>
+            </tr>
+          </thead>
+          <tbody>
+            {posts.map(p => (
+              <tr key={p.id}>
+                <td>{p.id}</td>
+                <td>{p.title}</td>
+                <td>{p.status}</td>
+                <td>{new Date(p.created_at).toLocaleString()}</td>
+                <td>
+                  <button onClick={() => handleEdit(p.id)}>编辑</button>
+                  <button onClick={() => handleDelete(p.id)}>删除</button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      )}
+    </div>
+  )
+}