上传和查看帖子
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