帖子的相关用户前端与管理员后端

Change-Id: I957181738817aeeaa89fabe23ceaf59d00c37a71
diff --git a/src/api/__tests__/post.test.js b/src/api/__tests__/post.test.js
new file mode 100644
index 0000000..89fe48f
--- /dev/null
+++ b/src/api/__tests__/post.test.js
@@ -0,0 +1,160 @@
+const axios = require('axios');
+const MockAdapter = require('axios-mock-adapter');
+
+const {
+    createPost,
+    togglePinPost,
+    deletePost,
+    updatePost,
+    searchPosts,
+    likePost,
+    unlikePost,
+    pinPost,
+    unpinPost,
+    findPostsByUserId,
+    findPinnedPosts,
+    getAllPostsSorted,
+    getPostById
+} = require('../post'); // 注意根据你的实际路径修改
+
+jest.mock('axios');
+
+describe('Post API Tests', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+    });
+
+    test('createPost should post form data', async () => {
+        const formData = new FormData();
+        formData.append('userid', '1');
+        formData.append('post_title', 'Test');
+        axios.post.mockResolvedValue({ data: true });
+
+        const response = await createPost(formData);
+
+        expect(axios.post).toHaveBeenCalledWith(
+            'http://localhost:8080/post/create',
+            formData,
+            { headers: { 'Content-Type': 'multipart/form-data' } }
+        );
+        expect(response.data).toBe(true);
+    });
+
+    test('togglePinPost should send PUT request', async () => {
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await togglePinPost(123);
+
+        expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/post/togglePin/123');
+        expect(response.data).toBe(true);
+    });
+
+    test('deletePost should send DELETE request', async () => {
+        axios.delete.mockResolvedValue({ data: true });
+
+        const response = await deletePost(123);
+
+        expect(axios.delete).toHaveBeenCalledWith('http://localhost:8080/post/delete/123');
+        expect(response.data).toBe(true);
+    });
+
+    test('updatePost should send PUT request with JSON', async () => {
+        const post = { postid: 1, post_title: 'Updated Title' };
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await updatePost(post);
+
+        expect(axios.put).toHaveBeenCalledWith(
+            'http://localhost:8080/post/update',
+            JSON.stringify(post),
+            { headers: { 'Content-Type': 'application/json' } }
+        );
+        expect(response.data).toBe(true);
+    });
+
+    test('searchPosts should send GET request with keyword', async () => {
+        const mockData = { data: [{ post_title: 'Keyword Match' }] };
+        axios.get.mockResolvedValue(mockData);
+
+        const response = await searchPosts('test');
+
+        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/post/search?keyword=test');
+        expect(response.data).toEqual(mockData.data);
+    });
+
+    test('likePost should send PUT request', async () => {
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await likePost(1);
+
+        expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/post/like/1');
+        expect(response.data).toBe(true);
+    });
+
+    test('unlikePost should send PUT request', async () => {
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await unlikePost(1);
+
+        expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/post/unlike/1');
+        expect(response.data).toBe(true);
+    });
+
+    test('pinPost should send PUT request', async () => {
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await pinPost(1);
+
+        expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/post/pin/1');
+        expect(response.data).toBe(true);
+    });
+
+    test('unpinPost should send PUT request', async () => {
+        axios.put.mockResolvedValue({ data: true });
+
+        const response = await unpinPost(1);
+
+        expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/post/unpin/1');
+        expect(response.data).toBe(true);
+    });
+
+    test('findPostsByUserId should fetch user posts', async () => {
+        const mockData = { data: [{ postid: 1, userid: 2 }] };
+        axios.get.mockResolvedValue(mockData);
+
+        const response = await findPostsByUserId(2);
+
+        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/post/findByUserid?userid=2');
+        expect(response.data).toEqual(mockData.data);
+    });
+
+    test('findPinnedPosts should fetch pinned posts', async () => {
+        const mockData = { data: [{ postid: 1, is_pinned: 1 }] };
+        axios.get.mockResolvedValue(mockData);
+
+        const response = await findPinnedPosts();
+
+        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/post/findPinned');
+        expect(response.data).toEqual(mockData.data);
+    });
+
+    test('getAllPostsSorted should fetch all posts sorted', async () => {
+        const mockData = { data: [{ postid: 1 }] };
+        axios.get.mockResolvedValue(mockData);
+
+        const response = await getAllPostsSorted();
+
+        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/post/all');
+        expect(response.data).toEqual(mockData.data);
+    });
+
+    test('getPostById should fetch post by ID', async () => {
+        const mockData = { data: { postid: 1 } };
+        axios.get.mockResolvedValue(mockData);
+
+        const response = await getPostById(1);
+
+        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/post/get/1');
+        expect(response.data).toEqual(mockData.data);
+    });
+});
diff --git a/src/api/post.js b/src/api/post.js
new file mode 100644
index 0000000..5891f99
--- /dev/null
+++ b/src/api/post.js
@@ -0,0 +1,128 @@
+const BASE_URL = 'http://localhost:8080/post';
+
+/**
+ * 创建帖子(带图片)
+ * @param {FormData} formData 包含 userid、post_title、post_content、tags、rannge、is_pinned、photo
+ */
+export const createPost = (formData) => {
+    return fetch(`${BASE_URL}/create`, {
+        method: 'POST',
+        body: formData,
+    }).then(res => res.json());
+};
+
+/**
+ * 切换置顶状态
+ * @param {number} postid 帖子 ID
+ */
+export const togglePinPost = (postid) => {
+    return fetch(`${BASE_URL}/togglePin/${postid}`, {
+        method: 'PUT',
+    }).then(res => res.json());
+};
+
+/**
+ * 删除帖子
+ * @param {number} postid 帖子 ID
+ */
+export const deletePost = (postid) => {
+    return fetch(`${BASE_URL}/delete/${postid}`, {
+        method: 'DELETE',
+    }).then(res => res.json());
+};
+
+/**
+ * 更新帖子(JSON 格式)
+ * @param {Object} post 帖子对象
+ */
+export const updatePost = (post) => {
+    return fetch(`${BASE_URL}/update`, {
+        method: 'PUT',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(post),
+    }).then(res => res.json());
+};
+
+/**
+ * 关键词搜索帖子
+ * @param {string} keyword 搜索关键词
+ */
+export const searchPosts = (keyword) => {
+    return fetch(`${BASE_URL}/search?keyword=${encodeURIComponent(keyword)}`)
+        .then(res => res.json());
+};
+
+/**
+ * 点赞帖子
+ * @param {number} postid 帖子 ID
+ */
+export const likePost = (postid) => {
+    return fetch(`${BASE_URL}/like/${postid}`, {
+        method: 'PUT',
+    }).then(res => res.json());
+};
+
+/**
+ * 取消点赞帖子
+ * @param {number} postid 帖子 ID
+ */
+export const unlikePost = (postid) => {
+    return fetch(`${BASE_URL}/unlike/${postid}`, {
+        method: 'PUT',
+    }).then(res => res.json());
+};
+
+/**
+ * 置顶帖子
+ * @param {number} postid 帖子 ID
+ */
+export const pinPost = (postid) => {
+    return fetch(`${BASE_URL}/pin/${postid}`, {
+        method: 'PUT',
+    }).then(res => res.json());
+};
+
+/**
+ * 取消置顶帖子
+ * @param {number} postid 帖子 ID
+ */
+export const unpinPost = (postid) => {
+    return fetch(`${BASE_URL}/unpin/${postid}`, {
+        method: 'PUT',
+    }).then(res => res.json());
+};
+
+/**
+ * 获取某用户所有帖子
+ * @param {number} userid 用户 ID
+ */
+export const findPostsByUserId = (userid) => {
+    return fetch(`${BASE_URL}/findByUserid?userid=${userid}`)
+        .then(res => res.json());
+};
+
+/**
+ * 获取所有置顶帖子
+ */
+export const findPinnedPosts = () => {
+    return fetch(`${BASE_URL}/findPinned`)
+        .then(res => res.json());
+};
+
+/**
+ * 获取所有帖子(排序后)
+ */
+export const getAllPostsSorted = () => {
+    return fetch(`${BASE_URL}/all`)
+        .then(res => res.json());
+};
+
+/**
+ * 根据 postid 获取帖子详情(若你后期需要)
+ */
+export const getPostById = (postid) => {
+    return fetch(`${BASE_URL}/get/${postid}`)
+        .then(res => res.json());
+};
diff --git a/src/components/Post.css b/src/components/Post.css
new file mode 100644
index 0000000..40d787e
--- /dev/null
+++ b/src/components/Post.css
@@ -0,0 +1,483 @@
+.post-container {
+  padding: 2rem;
+  background: #f8f9fa;
+  min-height: 100vh;
+  max-width: 900px;
+  margin: auto;
+}
+
+.post-form-card {
+  background: white;
+  border-radius: 12px;
+  padding: 2rem;
+  box-shadow: 0 8px 20px rgba(255, 102, 0, 0.15);
+  margin-bottom: 2rem;
+  animation: fadeInUp 0.5s ease-in-out;
+}
+
+.post-form-title {
+  font-size: 1.5rem;
+  font-weight: bold;
+  margin-bottom: 1rem;
+  color: #ff6600;
+}
+
+.post-input,
+.post-textarea {
+  width: 100%;
+  padding: 12px 14px;
+  border-radius: 8px;
+  border: 1px solid #ffbf80;
+  margin-bottom: 1rem;
+  font-size: 1rem;
+  transition: border-color 0.3s ease;
+}
+
+.post-input:focus,
+.post-textarea:focus {
+  outline: none;
+  border-color: #ff6600;
+  box-shadow: 0 0 8px rgba(255, 102, 0, 0.5);
+}
+
+.post-textarea {
+  height: 100px;
+  resize: vertical;
+}
+
+.post-file {
+  margin-bottom: 1rem;
+}
+
+.post-button {
+  padding: 10px 20px;
+  border: none;
+  border-radius: 8px;
+  font-size: 1rem;
+  font-weight: 600;
+  margin-right: 0.5rem;
+  transition: all 0.3s ease;
+  cursor: pointer;
+  box-shadow: 0 4px 6px rgba(255, 102, 0, 0.3);
+}
+
+/* 浅橙色实底按钮 */
+.primary {
+  background: linear-gradient(45deg, #FFB366, #FFC299);
+  color: white;
+  box-shadow: 0 6px 10px rgba(255, 179, 102, 0.5);
+}
+
+.primary:hover {
+  background: linear-gradient(45deg, #FFAA33, #FFBF66);
+  box-shadow: 0 8px 14px rgba(255, 170, 51, 0.7);
+}
+
+/* 橙色边框按钮 */
+.primary-outline {
+  background: transparent;
+  border: 2px solid #ff6600;
+  color: #ff6600;
+  box-shadow: none;
+}
+
+.primary-outline:hover {
+  background: #ff6600;
+  color: white;
+  box-shadow: 0 6px 10px rgba(255, 102, 0, 0.4);
+}
+
+.post-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1.5rem;
+}
+
+.post-card {
+  background: white;
+  border-radius: 12px;
+  padding: 1.5rem;
+  box-shadow: 0 4px 15px rgba(255, 102, 0, 0.12);
+  animation: fadeInUp 0.3s ease-out;
+  transition: box-shadow 0.3s ease;
+}
+
+.post-card:hover {
+  box-shadow: 0 8px 30px rgba(255, 102, 0, 0.25);
+}
+
+.post-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 1rem;
+}
+
+.post-info {
+  flex: 1;
+}
+
+.post-title {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #cc5200;
+  margin-bottom: 0.75rem;
+  padding-left: 12px;
+  border-left: 4px solid #ff6600;
+  transition: color 0.3s ease;
+}
+
+.post-title:hover {
+  color: #ff6600;
+}
+
+.post-content {
+  color: #444;
+  margin-bottom: 0.75rem;
+}
+
+.post-image {
+  width: 160px;
+  height: 120px;
+  object-fit: cover;
+  border-radius: 6px;
+  box-shadow: 0 4px 8px rgba(255, 102, 0, 0.3);
+  flex-shrink: 0;
+  cursor: pointer;
+  transition: transform 0.3s ease;
+}
+
+.post-image:hover {
+  transform: scale(1.05);
+}
+
+.post-meta {
+  font-size: 0.9rem;
+  color: #777;
+  margin-top: 0.5rem;
+}
+
+.post-actions-inline {
+  margin-top: 0.75rem;
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+  font-size: 0.95rem;
+}
+
+.post-link {
+  cursor: pointer;
+  text-decoration: underline;
+  transition: color 0.3s ease;
+}
+
+.post-link.like {
+  color: #ff6600;
+}
+
+.post-link.like:hover {
+  color: #ff8533;
+}
+
+.post-link.unlike {
+  color: #cc3300;
+}
+
+.post-link.unlike:hover {
+  color: #ff3300;
+}
+
+.post-comment {
+  margin-top: 1rem;
+  border-top: 1px solid #ffe6cc;
+  padding-top: 1rem;
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* 图片模态框 */
+.image-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(0, 0, 0, 0.75);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 9999;
+}
+
+.image-modal-content {
+  position: relative;
+  max-width: 90%;
+  max-height: 90%;
+}
+
+.image-modal-img {
+  width: 100%;
+  max-height: 80vh;
+  border-radius: 12px;
+  box-shadow: 0 0 20px rgba(255, 102, 0, 0.8);
+}
+
+.image-modal-close {
+  position: absolute;
+  top: -20px;
+  right: -20px;
+  background: white;
+  border: none;
+  border-radius: 50%;
+  width: 36px;
+  height: 36px;
+  font-size: 24px;
+  line-height: 36px;
+  text-align: center;
+  cursor: pointer;
+  box-shadow: 0 2px 10px rgba(255, 102, 0, 0.7);
+}
+
+.action-group {
+  display: flex;
+  gap: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+.post-empty-state {
+  text-align: center;
+  padding: 2rem;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(255, 102, 0, 0.12);
+}
+
+.post-empty-message {
+  font-size: 1.2rem;
+  color: #cc5200;
+  margin-bottom: 1rem;
+}
+
+/* RequestBoard组件样式保持一致 */
+.request-board-container {
+  background: white;
+  border-radius: 12px;
+  padding: 1.5rem;
+  box-shadow: 0 4px 15px rgba(255, 102, 0, 0.12);
+  margin-top: 1rem;
+}
+
+.request-board-section {
+  margin-bottom: 1.5rem;
+}
+
+.request-board-form {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 1rem;
+  margin-bottom: 1.5rem;
+}
+
+.request-board-input {
+  padding: 10px 12px;
+  border-radius: 8px;
+  border: 1px solid #ffbf80;
+  font-size: 0.95rem;
+}
+
+.request-board-input:focus {
+  border-color: #ff6600;
+  box-shadow: 0 0 8px rgba(255, 102, 0, 0.5);
+  outline: none;
+}
+
+.request-board-textarea {
+  grid-column: span 2;
+  padding: 10px 12px;
+  border-radius: 8px;
+  border: 1px solid #ffbf80;
+  resize: vertical;
+  min-height: 100px;
+}
+
+.request-board-textarea:focus {
+  border-color: #ff6600;
+  box-shadow: 0 0 8px rgba(255, 102, 0, 0.5);
+  outline: none;
+}
+
+.request-board-file {
+  grid-column: span 2;
+}
+
+.request-board-list {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 1rem;
+}
+
+.request-board-item {
+  background: white;
+  border-radius: 10px;
+  padding: 1.25rem;
+  box-shadow: 0 2px 8px rgba(255, 102, 0, 0.08);
+}
+
+.request-board-item-title {
+  font-size: 1.3rem;
+  font-weight: 700;
+  margin-bottom: 0.75rem;
+  color: hsl(24, 67%, 55%);
+  border-bottom: 2px solid #ffe0b3;
+  padding-bottom: 0.5rem;
+}
+
+.request-board-item-content {
+  color: #555;
+  margin-bottom: 1rem;
+}
+
+.request-board-item-meta {
+  display: flex;
+  gap: 1rem;
+  margin-bottom: 1rem;
+  color: #777;
+  font-size: 0.9rem;
+}
+
+.request-board-item-image {
+  max-width: 200px;
+  max-height: 150px;
+  border-radius: 6px;
+  margin-bottom: 1rem;
+}
+
+.request-board-item-actions {
+  display: flex;
+  gap: 0.75rem;
+  flex-wrap: wrap;
+}
+
+.request-board-small-input {
+  padding: 8px 10px;
+  border-radius: 6px;
+  border: 1px solid #ffbf80;
+  flex: 1;
+}
+
+.request-board-small-input:focus {
+  border-color: #ff6600;
+  box-shadow: 0 0 8px rgba(255, 102, 0, 0.5);
+  outline: none;
+}
+
+/* 新增的用户信息显示样式 */
+.post-form-user-info {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 10px;
+  background-color: #f8f9fa;
+  border-radius: 8px;
+  border: 1px solid #e9ecef;
+}
+
+.post-form-user-avatar {
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  object-fit: cover;
+  margin-right: 15px;
+  border: 2px solid #40a9ff;
+}
+
+.post-form-user-avatar-placeholder {
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background-color: #e9ecef;
+  margin-right: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 2px solid #40a9ff;
+}
+
+.avatar-initial {
+  font-size: 24px;
+  font-weight: bold;
+  color: #495057;
+}
+
+.post-form-username {
+  font-weight: 600;
+  font-size: 18px;
+  color: #212529;
+}
+
+.post-form-user-label {
+  margin-left: auto;
+  padding: 5px 10px;
+  background-color: #e6f7ff;
+  color: #1890ff;
+  border-radius: 4px;
+  font-size: 14px;
+}
+
+/* 帖子作者信息样式 */
+.post-author-info {
+  display: flex;
+  align-items: center;
+  padding: 10px 15px;
+  border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 15px;
+}
+
+.post-author-avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  object-fit: cover;
+  margin-right: 12px;
+  border: 1px solid #e8e8e8;
+}
+
+.post-author-avatar-placeholder {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background-color: #f5f5f5;
+  margin-right: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #e8e8e8;
+}
+
+.post-author-details {
+  display: flex;
+  flex-direction: column;
+}
+
+.post-author-name {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 3px;
+  color: #333;
+}
+
+.post-meta {
+  font-size: 12px;
+  color: #888;
+  margin: 2px 0;
+}
\ No newline at end of file
diff --git a/src/components/Post.jsx b/src/components/Post.jsx
new file mode 100644
index 0000000..c6b2e69
--- /dev/null
+++ b/src/components/Post.jsx
@@ -0,0 +1,294 @@
+import React, { useState, useEffect } from 'react';
+import {
+    createPost,
+    findPinnedPosts,
+    likePost,
+    unlikePost,
+    searchPosts,
+    getAllPostsSorted,
+    findPostsByUserId,
+} from '../api/post';
+import Comment from './Comment';
+import RequestBoard from './RequestBoard';
+import './Post.css';
+
+const Post = () => {
+    const [title, setTitle] = useState('');
+    const [content, setContent] = useState('');
+    const [tags, setTags] = useState('');
+    const [photo, setPhoto] = useState(null);
+    const [posts, setPosts] = useState([]);
+    const [searchKeyword, setSearchKeyword] = useState('');
+    const [imagePreview, setImagePreview] = useState(null);
+    const [showCreateForm, setShowCreateForm] = useState(false);
+    const [currentView, setCurrentView] = useState('posts');
+    const [currentUser, setCurrentUser] = useState(null);
+    const [userDecorations, setUserDecorations] = useState({});
+
+    useEffect(() => {
+        const storedUser = JSON.parse(localStorage.getItem('user'));
+        if (storedUser) setCurrentUser(storedUser);
+    }, []);
+
+    useEffect(() => {
+        loadPinnedPosts();
+    }, []);
+
+    useEffect(() => {
+        if (posts.length > 0) {
+            fetchUserDecorations(posts);
+        }
+    }, [posts]);
+
+    const fetchUserDecorations = async (posts) => {
+        const decorations = {};
+        await Promise.all(
+            posts.map(async (post) => {
+                if (!decorations[post.userid]) {
+                    try {
+                        const res = await fetch(`http://localhost:8080/user/getDecoration?userid=${post.userid}`);
+                        const json = await res.json();
+                        if (json.success) {
+                            decorations[post.userid] = json.data;
+                        }
+                    } catch (error) {
+                        console.error(`获取用户 ${post.userid} 的装饰信息失败`, error);
+                    }
+                }
+            })
+        );
+        setUserDecorations(decorations);
+    };
+
+    const loadPinnedPosts = async () => {
+        const data = await findPinnedPosts();
+        setPosts(data);
+        setCurrentView('posts');
+    };
+
+    const loadAllPosts = async () => {
+        const data = await getAllPostsSorted();
+        setPosts(data);
+        setCurrentView('posts');
+    };
+
+    const loadMyPosts = async () => {
+        if (!currentUser) return;
+        const data = await findPostsByUserId(currentUser.userid);
+        setPosts(data);
+        setCurrentView('posts');
+    };
+
+    const handleCreate = async () => {
+        if (!currentUser) {
+            alert('请先登录再发帖');
+            return;
+        }
+
+        const formData = new FormData();
+        formData.append('userid', currentUser.userid);
+        formData.append('post_title', title);
+        formData.append('post_content', content);
+        formData.append('tags', tags);
+        formData.append('rannge', 'public');
+        if (photo) formData.append('photo', photo);
+
+        const success = await createPost(formData);
+        if (success) {
+            alert('帖子创建成功');
+            loadPinnedPosts();
+            setTitle('');
+            setContent('');
+            setTags('');
+            setPhoto(null);
+            setShowCreateForm(false);
+        } else {
+            alert('创建失败');
+        }
+    };
+
+    const handleLike = async (postid) => {
+        await likePost(postid);
+        loadPinnedPosts();
+    };
+
+    const handleUnlike = async (postid) => {
+        await unlikePost(postid);
+        loadPinnedPosts();
+    };
+
+    const handleSearch = async () => {
+        const result = await searchPosts(searchKeyword);
+        setPosts(result);
+        setCurrentView('posts');
+    };
+
+    const showRequestBoard = () => {
+        setCurrentView('requests');
+    };
+
+    return (
+        <div className="post-container">
+            <div className="post-actions">
+                <div className="action-group">
+                    {currentUser && (
+                        <button
+                            className="post-button primary"
+                            onClick={() => setShowCreateForm(!showCreateForm)}
+                        >
+                            {showCreateForm ? '收起发布表单' : '创建新帖子'}
+                        </button>
+                    )}
+                    <input
+                        type="text"
+                        placeholder="搜索关键词"
+                        value={searchKeyword}
+                        onChange={(e) => setSearchKeyword(e.target.value)}
+                        className="post-input"
+                    />
+                    <button className="post-button primary-outline" onClick={handleSearch}>搜索</button>
+                </div>
+
+                <div className="action-group">
+                    <button className="post-button primary-outline" onClick={loadPinnedPosts}>置顶帖子</button>
+                    <button className="post-button primary-outline" onClick={loadAllPosts}>所有帖子</button>
+                    {currentUser && (
+                        <button className="post-button primary-outline" onClick={loadMyPosts}>我的帖子</button>
+                    )}
+                    <button className="post-button primary-outline" onClick={showRequestBoard}>需求帖子</button>
+                </div>
+            </div>
+
+            {showCreateForm && currentUser && currentView !== 'requests' && (
+                <div className="post-form-card">
+                    <div className="post-form-user-info">
+                        {currentUser.image ? (
+                            <img
+                                src={currentUser.image}
+                                alt="头像"
+                                className="post-form-user-avatar"
+                            />
+                        ) : (
+                            <div className="post-form-user-avatar-placeholder">
+                                <div className="avatar-initial">
+                                    {currentUser.username?.charAt(0) || 'U'}
+                                </div>
+                            </div>
+                        )}
+                        <div className="post-form-username">{currentUser.username}</div>
+                        <div className="post-form-user-label">发布者</div>
+                    </div>
+
+                    <h2 className="post-form-title">发布新帖子</h2>
+                    <input
+                        type="text"
+                        placeholder="标题"
+                        value={title}
+                        onChange={(e) => setTitle(e.target.value)}
+                        className="post-input"
+                    />
+                    <textarea
+                        placeholder="内容"
+                        value={content}
+                        onChange={(e) => setContent(e.target.value)}
+                        className="post-textarea"
+                    />
+                    <input
+                        type="text"
+                        placeholder="标签(逗号分隔)"
+                        value={tags}
+                        onChange={(e) => setTags(e.target.value)}
+                        className="post-input"
+                    />
+                    <input
+                        type="file"
+                        onChange={(e) => setPhoto(e.target.files[0])}
+                        className="post-file"
+                    />
+                    <button className="post-button primary" onClick={handleCreate}>
+                        发布
+                    </button>
+                </div>
+            )}
+
+            {currentView === 'requests' ? (
+                <RequestBoard
+                    currentUserId={currentUser?.userid}
+                    onBack={() => setCurrentView('posts')}
+                />
+            ) : (
+                posts.length > 0 ? (
+                    <div className="post-list">
+                        {posts.map((post) => {
+                            const decoration = userDecorations[post.userid] || {};
+                            const avatar = decoration.image || '';
+                            const username = decoration.username || '匿名用户';
+
+                            return (
+                                <div className="post-card" key={post.postid}>
+                                    <div className="post-author-info">
+                                        {avatar ? (
+                                            <img
+                                                src={avatar}
+                                                alt="头像"
+                                                className="post-author-avatar"
+                                            />
+                                        ) : (
+                                            <div className="post-author-avatar-placeholder">
+                                                <div className="avatar-initial">{username.charAt(0)}</div>
+                                            </div>
+                                        )}
+                                        <div className="post-author-details">
+                                            <div className="post-author-name">{username}</div>
+                                            <div className="post-meta">发布时间: {post.postCreatedTime}</div>
+                                        </div>
+                                    </div>
+
+                                    <div className="post-header">
+                                        <div className="post-info">
+                                            <h3 className="post-title">{post.postTitle}</h3>
+                                            <p className="post-content">{post.postContent}</p>
+                                            <div className="post-meta">标签: {post.tags || '无标签'}</div>
+                                            <div className="post-actions-inline">
+                                                <span>👍 {post.likes}</span>
+                                                <button className="post-link like" onClick={() => handleLike(post.postid)}>点赞</button>
+                                                <button className="post-link unlike" onClick={() => handleUnlike(post.postid)}>取消点赞</button>
+                                            </div>
+                                        </div>
+                                        {post.photo && (
+                                            <img
+                                                src={`http://localhost:8080${post.photo}`}
+                                                alt="post"
+                                                className="post-image"
+                                                onClick={() => setImagePreview(`http://localhost:8080${post.photo}`)}
+                                            />
+                                        )}
+                                    </div>
+
+                                    <div className="post-comment">
+                                        <Comment postId={post.postid} currentUser={currentUser} />
+                                    </div>
+                                </div>
+                            );
+                        })}
+                    </div>
+                ) : (
+                    <div className="post-empty-state">
+                        <p className="post-empty-message">暂无内容</p>
+                    </div>
+                )
+            )}
+
+            {imagePreview && (
+                <div className="image-modal" onClick={() => setImagePreview(null)}>
+                    <div className="image-modal-content" onClick={(e) => e.stopPropagation()}>
+                        <img src={imagePreview} alt="preview" className="image-modal-img" />
+                        <button className="image-modal-close" onClick={() => setImagePreview(null)}>×</button>
+                    </div>
+                </div>
+            )}
+        </div>
+    );
+};
+
+export default Post;
diff --git a/src/components/PostAdminPanel.jsx b/src/components/PostAdminPanel.jsx
new file mode 100644
index 0000000..a01df08
--- /dev/null
+++ b/src/components/PostAdminPanel.jsx
@@ -0,0 +1,232 @@
+import React, { useEffect, useState } from 'react';
+import {
+    Table,
+    Button,
+    Modal,
+    Image,
+    message,
+    Tag,
+    Space,
+    Input,
+    Tooltip,
+} from 'antd';
+import { ExclamationCircleOutlined, SearchOutlined } from '@ant-design/icons';
+import {
+    getAllPostsSorted,
+    searchPosts,
+    deletePost,
+    togglePinPost,
+} from '../api/post';
+
+const { confirm } = Modal;
+
+const PostAdminPanel = () => {
+    const [posts, setPosts] = useState([]);
+    const [loading, setLoading] = useState(false);
+    const [keyword, setKeyword] = useState('');
+
+    const fetchPosts = async () => {
+        setLoading(true);
+        try {
+            const data = keyword.trim()
+                ? await searchPosts(keyword.trim())
+                : await getAllPostsSorted();
+            setPosts(data);
+        } catch (error) {
+            message.error('获取帖子失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    useEffect(() => {
+        fetchPosts();
+    }, []);
+
+    const handleSearch = () => {
+        fetchPosts();
+    };
+
+    const handleDelete = (postid) => {
+        confirm({
+            title: '确认删除该帖子吗?',
+            icon: <ExclamationCircleOutlined />,
+            okText: '删除',
+            okType: 'danger',
+            cancelText: '取消',
+            onOk: async () => {
+                try {
+                    const res = await deletePost(postid);
+                    if (res) {
+                        message.success('删除成功');
+                        fetchPosts();
+                    } else {
+                        message.error('删除失败');
+                    }
+                } catch {
+                    message.error('删除请求失败');
+                }
+            },
+        });
+    };
+
+    const handlePinToggle = async (postid) => {
+        try {
+            const res = await togglePinPost(postid);
+            if (res === true || res === false) {
+                // ✅ 用后端返回的状态更新 UI
+                const newPosts = posts.map(post => {
+                    if (post.postid === postid) {
+                        return {
+                            ...post,
+                            is_pinned: res ? 1 : 0,
+                        };
+                    }
+                    return post;
+                });
+                setPosts(newPosts);
+                message.success(`帖子${res ? '已置顶' : '已取消置顶'}`);
+            } else {
+                message.error('更新失败');
+            }
+        } catch {
+            message.error('更新置顶状态失败');
+        }
+    };
+
+
+
+
+    const columns = [
+        {
+            title: 'ID',
+            dataIndex: 'postid',
+            key: 'postid',
+            width: 80,
+            fixed: 'left',
+        },
+        {
+            title: '用户ID',
+            dataIndex: 'userid',
+            key: 'userid',
+            width: 100,
+        },
+        {
+            title: '标题',
+            dataIndex: 'postTitle',
+            key: 'postTitle',
+            width: 200,
+            ellipsis: true,
+        },
+        {
+            title: '内容',
+            dataIndex: 'postContent',
+            key: 'postContent',
+            ellipsis: { showTitle: false },
+            render: (text) => (
+                <Tooltip placement="topLeft" title={text}>
+                    {text}
+                </Tooltip>
+            ),
+        },
+        {
+            title: '标签',
+            dataIndex: 'tags',
+            key: 'tags',
+            render: (tags) =>
+                tags ? tags.split(',').map((tag) => <Tag key={tag}>{tag}</Tag>) : <Tag color="default">无</Tag>,
+        },
+        {
+            title: '图片',
+            dataIndex: 'photo',
+            key: 'photo',
+            width: 100,
+            render: (url) =>
+                url ? (
+                    <Image
+                        src={`http://localhost:8080${url}`}
+                        width={80}
+                        height={80}
+                        style={{ objectFit: 'cover' }}
+                    />
+                ) : (
+                    <Tag color="default">无</Tag>
+                ),
+        },
+        {
+            title: '点赞数',
+            dataIndex: 'likes',
+            key: 'likes',
+            width: 100,
+            render: (likes) => <Tag color="blue">{likes}</Tag>,
+        },
+        {
+            title: '置顶状态',
+            dataIndex: 'is_pinned',
+            key: 'is_pinned',
+            width: 100,
+            render: (val) =>
+                val ? <Tag color="green">已置顶</Tag> : <Tag color="default">未置顶</Tag>,
+        },
+        {
+            title: '发布时间',
+            dataIndex: 'postCreatedTime',
+            key: 'postCreatedTime',
+            width: 180,
+            render: (time) =>
+                time ? new Date(time).toLocaleString() : <Tag color="default">暂无</Tag>,
+        },
+        {
+            title: '操作',
+            key: 'action',
+            width: 180,
+            fixed: 'right',
+            render: (_, record) => (
+                <Space size="middle">
+                    <Button
+                        type="primary"
+                        onClick={() => handlePinToggle(record.postid, record.is_pinned)}
+                    >
+                        {Boolean(record.is_pinned) ? '取消置顶' : '置顶'}
+                    </Button>
+
+                    <Button danger onClick={() => handleDelete(record.postid)}>
+                        删除
+                    </Button>
+                </Space>
+            ),
+        },
+    ];
+
+    return (
+        <div style={{ padding: 20 }}>
+            <h2 style={{ marginBottom: 20 }}>帖子管理面板</h2>
+            <Space style={{ marginBottom: 16 }}>
+                <Input
+                    placeholder="请输入关键词"
+                    value={keyword}
+                    onChange={(e) => setKeyword(e.target.value)}
+                    style={{ width: 300 }}
+                />
+                <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
+                    搜索
+                </Button>
+                <Button onClick={() => { setKeyword(''); fetchPosts(); }}>
+                    重置
+                </Button>
+            </Space>
+            <Table
+                rowKey="postid"
+                columns={columns}
+                dataSource={posts}
+                loading={loading}
+                scroll={{ x: 1600 }}
+                pagination={{ pageSize: 10 }}
+                bordered
+                size="middle"
+            />
+        </div>
+    );
+};
+
+export default PostAdminPanel;
diff --git a/src/components/PostCard.jsx b/src/components/PostCard.jsx
new file mode 100644
index 0000000..5051cec
--- /dev/null
+++ b/src/components/PostCard.jsx
@@ -0,0 +1,103 @@
+import React, { useState, useEffect } from 'react';
+import {
+    likePost,
+    unlikePost,
+    togglePinPost,
+    deletePost,
+} from '../api/post';
+
+const PostCard = ({ post, onDeleted, onUpdated }) => {
+    const [likes, setLikes] = useState(post.likes);
+    const [isLiked, setIsLiked] = useState(false); // 默认未点赞
+    const [isPinned, setIsPinned] = useState(post.is_pinned || false);
+    const [loading, setLoading] = useState(false);
+
+    useEffect(() => {
+        setIsPinned(post.is_pinned || false);
+    }, [post.is_pinned]);
+
+    const handleLike = async () => {
+        setLoading(true);
+        try {
+            if (!isLiked) {
+                const res = await likePost(post.postid);
+                if (res) {
+                    setLikes(likes + 1);
+                    setIsLiked(true);
+                }
+            } else {
+                const res = await unlikePost(post.postid);
+                if (res) {
+                    setLikes(likes - 1);
+                    setIsLiked(false);
+                }
+            }
+        } catch (error) {
+            console.error('点赞操作失败', error);
+        }
+        setLoading(false);
+    };
+
+    const handlePin = async () => {
+        setLoading(true);
+        try {
+            const res = await togglePinPost(post.postid);
+            if (res) {
+                onUpdated && onUpdated(); // 通知父组件刷新数据
+            }
+        } catch (error) {
+            console.error('置顶切换失败', error);
+        }
+        setLoading(false);
+    };
+
+    const handleDelete = async () => {
+        if (!window.confirm('确认删除该帖子吗?')) return;
+        setLoading(true);
+        try {
+            const res = await deletePost(post.postid);
+            if (res) {
+                onDeleted && onDeleted(post.postid);
+            }
+        } catch (error) {
+            console.error('删除失败', error);
+        }
+        setLoading(false);
+    };
+
+    return (
+        <div className="post-card" style={{ border: '1px solid #ddd', padding: 16, marginBottom: 12 }}>
+            <h3>{post.postTitle}</h3>
+            <p>{post.postContent}</p>
+            {post.photo && (
+                <img
+                    src={`http://localhost:8080${post.photo}`}
+                    alt="帖子图片"
+                    style={{ maxWidth: '100%', maxHeight: 200 }}
+                />
+            )}
+            <p>
+                <strong>标签:</strong> {post.tags || '无'}
+            </p>
+            <p>
+                <strong>作者ID:</strong> {post.userid} | <strong>点赞数:</strong> {likes}
+            </p>
+            <p>
+                <strong>发布时间:</strong> {new Date(post.postCreatedTime).toLocaleString()}
+            </p>
+            <p>
+                <button onClick={handleLike} disabled={loading}>
+                    {isLiked ? '取消点赞' : '点赞'}
+                </button>{' '}
+                <button onClick={handlePin} disabled={loading}>
+                    {isPinned ? '取消置顶' : '置顶'}
+                </button>{' '}
+                <button onClick={handleDelete} disabled={loading}>
+                    删除
+                </button>
+            </p>
+        </div>
+    );
+};
+
+export default PostCard;
diff --git a/src/pages/Community.jsx b/src/pages/Community.jsx
new file mode 100644
index 0000000..38d6623
--- /dev/null
+++ b/src/pages/Community.jsx
@@ -0,0 +1,53 @@
+// src/pages/Home.jsx
+import React, { useState, useEffect } from 'react';
+import { getActivityPreviews, getFullActivities } from '../api/activity';
+import Post from '../components/Post';
+import Navbar from '../components/Navbar'; // ✅ 导入导航栏组件
+import { AppstoreOutlined } from '@ant-design/icons';
+import { Row, Col, Card, Space } from 'antd';
+import './Home.css';
+
+const Community = () => {
+    const [activityPreviews, setActivityPreviews] = useState([]);
+    const [fullActivities, setFullActivities] = useState([]);
+    const [selectedActivityId, setSelectedActivityId] = useState(null);
+
+    useEffect(() => {
+        getActivityPreviews().then(res => setActivityPreviews(res.data));
+        getFullActivities().then(res => setFullActivities(res.data));
+    }, []);
+
+    return (
+        <div style={{ minHeight: '100vh', backgroundColor: '#f0f2f5' }}>
+            {/* 导航栏 */}
+            <Navbar />
+
+            {/* 内容区域 */}
+            <div style={{ padding: 24 }}>
+                {/* 帖子区域 */}
+                <Row justify="center">
+                    <Col
+                        xs={24}
+                        sm={24}
+                        md={24}
+                        lg={22}
+                        xl={20}
+                        xxl={18}
+                        style={{ maxWidth: 1400, width: '100%' }}
+                    >
+                        <Card
+                            title={<Space><AppstoreOutlined />最新帖子</Space>}
+                            bordered={false}
+                            bodyStyle={{ padding: 24 }}
+                            style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.15)', borderRadius: 8 }}
+                        >
+                            <Post />
+                        </Card>
+                    </Col>
+                </Row>
+            </div>
+        </div>
+    );
+};
+
+export default Community;