Merge "公告用户前端以及管理员"
diff --git a/src/api/activity.js b/src/api/activity.js
new file mode 100644
index 0000000..8d9b24f
--- /dev/null
+++ b/src/api/activity.js
@@ -0,0 +1,48 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/activity';
+
+// 获取所有 is_show == 0 的活动预览(只含标题和图片)
+export const getActivityPreviews = () => {
+    return axios.get(`${BASE_URL}/preview`);
+};
+
+// 获取所有 is_show == 0 的完整活动信息
+export const getFullActivities = () => {
+    return axios.get(`${BASE_URL}/full`);
+};
+
+// 创建新的活动公告(使用 FormData 传递)
+export const createActivity = (formData) => {
+    return axios.post(`${BASE_URL}/create`, formData, {
+        headers: {
+            'Content-Type': 'multipart/form-data',
+        },
+    });
+};
+
+// 删除活动公告(通过 ID)
+export const deleteActivity = (id) => {
+    return axios.delete(`${BASE_URL}/delete/${id}`);
+};
+
+// 获取所有活动(无论展示状态)
+export const getAllActivities = () => {
+    return axios.get(`${BASE_URL}/all`);
+};
+
+// 修改活动公告(使用 FormData 传递)
+export const updateActivity = (formData) => {
+    return axios.put(`${BASE_URL}/update`, formData, {
+        headers: {
+            'Content-Type': 'multipart/form-data',
+        },
+    });
+};
+
+// 根据标题搜索活动
+export const searchActivitiesByTitle = (title) => {
+    return axios.get(`${BASE_URL}/search`, {
+        params: { title },
+    });
+};
diff --git a/src/components/ActivityAdminPanel.jsx b/src/components/ActivityAdminPanel.jsx
new file mode 100644
index 0000000..5c0ce26
--- /dev/null
+++ b/src/components/ActivityAdminPanel.jsx
@@ -0,0 +1,269 @@
+import React, { useEffect, useState } from 'react';
+import {
+    Table, Button, Modal, Image, message, Tag,
+    Space, Input, Tooltip, Form, Upload
+} from 'antd';
+import {
+    ExclamationCircleOutlined, SearchOutlined, UploadOutlined
+} from '@ant-design/icons';
+import {
+    getAllActivities,
+    searchActivitiesByTitle,
+    deleteActivity,
+    createActivity,
+    updateActivity
+} from '../api/activity';
+
+const { confirm } = Modal;
+
+const ActivityAdminPanel = () => {
+    const [activities, setActivities] = useState([]);
+    const [loading, setLoading] = useState(false);
+    const [keyword, setKeyword] = useState('');
+    const [modalVisible, setModalVisible] = useState(false);
+    const [editMode, setEditMode] = useState(false);
+    const [currentActivity, setCurrentActivity] = useState(null);
+    const [form] = Form.useForm();
+
+    const fetchActivities = async () => {
+        setLoading(true);
+        try {
+            const data = keyword.trim()
+                ? await searchActivitiesByTitle(keyword.trim())
+                : await getAllActivities();
+            setActivities(data.data || []);
+        } catch (error) {
+            message.error('获取公告失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    useEffect(() => {
+        fetchActivities();
+    }, []);
+
+    const handleSearch = () => {
+        fetchActivities();
+    };
+
+    const handleDelete = (activityid) => {
+        confirm({
+            title: '确认删除该公告吗?',
+            icon: <ExclamationCircleOutlined />,
+            okText: '删除',
+            okType: 'danger',
+            cancelText: '取消',
+            onOk: async () => {
+                try {
+                    const res = await deleteActivity(activityid);
+                    if (res.data === true) {
+                        message.success('删除成功');
+                        fetchActivities();
+                    } else {
+                        message.error('删除失败');
+                    }
+                } catch {
+                    message.error('删除请求失败');
+                }
+            },
+        });
+    };
+
+    const openEditModal = (record) => {
+        setEditMode(true);
+        setCurrentActivity(record);
+        form.setFieldsValue({
+            activityid: record.activityid,
+            title: record.title,
+            content: record.content,
+            isShow: record.is_show,
+        });
+        setModalVisible(true);
+    };
+
+    const openCreateModal = () => {
+        setEditMode(false);
+        form.resetFields();
+        setModalVisible(true);
+    };
+
+    const handleModalOk = async () => {
+        try {
+            const values = await form.validateFields();
+            const formData = new FormData();
+            Object.keys(values).forEach(key => {
+                if (key !== 'photo' && values[key] !== undefined) {
+                    formData.append(key, values[key]);
+                }
+            });
+            if (values.photo && values.photo.file) {
+                formData.append('photo', values.photo.file);
+            }
+
+            const res = editMode
+                ? await updateActivity(formData)
+                : await createActivity(formData);
+
+            if (res.data === true) {
+                message.success(editMode ? '修改成功' : '创建成功');
+                setModalVisible(false);
+                fetchActivities();
+            } else {
+                message.error('提交失败');
+            }
+        } catch (error) {
+            message.error('提交失败,请检查表单');
+        }
+    };
+
+    const columns = [
+        {
+            title: 'ID',
+            dataIndex: 'activityid',
+            key: 'activityid',
+            width: 80,
+        },
+        {
+            title: '标题',
+            dataIndex: 'title',
+            key: 'title',
+            width: 200,
+            ellipsis: true,
+        },
+        {
+            title: '内容',
+            dataIndex: 'content',
+            key: 'content',
+            ellipsis: { showTitle: false },
+            render: (text) => (
+                <Tooltip placement="topLeft" title={text}>
+                    {text}
+                </Tooltip>
+            ),
+        },
+        {
+            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: 'is_show',
+            key: 'is_show',
+            width: 100,
+            render: (val) =>
+                val === 0 ? <Tag color="green">展示</Tag> : <Tag color="red">隐藏</Tag>,
+        },
+        {
+            title: '发布时间',
+            dataIndex: 'time',
+            key: 'time',
+            width: 180,
+            render: (time) =>
+                time ? new Date(time).toLocaleString() : <Tag color="default">暂无</Tag>,
+        },
+        {
+            title: '操作',
+            key: 'action',
+            width: 180,
+            render: (_, record) => (
+                <Space size="middle">
+                    <Button type="primary" onClick={() => openEditModal(record)}>
+                        修改
+                    </Button>
+                    <Button danger onClick={() => handleDelete(record.activityid)}>
+                        删除
+                    </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(''); fetchActivities(); }}>
+                    重置
+                </Button>
+                <Button type="primary" onClick={openCreateModal}>
+                    新建公告
+                </Button>
+            </Space>
+            <Table
+                rowKey="activityid"
+                columns={columns}
+                dataSource={activities}
+                loading={loading}
+                scroll={{ x: 1200 }}
+                pagination={{ pageSize: 8 }}
+                bordered
+                size="middle"
+            />
+
+            <Modal
+                title={editMode ? '修改公告' : '新建公告'}
+                open={modalVisible}
+                onOk={handleModalOk}
+                onCancel={() => setModalVisible(false)}
+                okText="提交"
+                cancelText="取消"
+                destroyOnClose
+            >
+                <Form form={form} layout="vertical" preserve={false}>
+                    {editMode && (
+                        <Form.Item name="activityid" hidden>
+                            <Input />
+                        </Form.Item>
+                    )}
+                    <Form.Item
+                        name="title"
+                        label="标题"
+                        rules={[{ required: true, message: '请输入标题' }]}
+                    >
+                        <Input />
+                    </Form.Item>
+                    <Form.Item
+                        name="content"
+                        label="内容"
+                        rules={[{ required: true, message: '请输入内容' }]}
+                    >
+                        <Input.TextArea rows={4} />
+                    </Form.Item>
+                    <Form.Item name="isShow" label="是否展示">
+                        <Input placeholder="0 表示展示,1 表示隐藏" />
+                    </Form.Item>
+                    <Form.Item name="photo" label="上传图片">
+                        <Upload beforeUpload={() => false} maxCount={1}>
+                            <Button icon={<UploadOutlined />}>选择文件</Button>
+                        </Upload>
+                    </Form.Item>
+                </Form>
+            </Modal>
+        </div>
+    );
+};
+
+export default ActivityAdminPanel;
diff --git a/src/components/ActivityBoard.css b/src/components/ActivityBoard.css
new file mode 100644
index 0000000..7cb11d7
--- /dev/null
+++ b/src/components/ActivityBoard.css
@@ -0,0 +1,38 @@
+/* src/components/ActivityBoard.css */
+
+.activity-card {
+    border-radius: 12px;
+    overflow: hidden;
+    transition: all 0.3s ease;
+}
+
+.activity-card:hover {
+    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
+    transform: translateY(-4px);
+}
+
+.activity-photo {
+    height: 180px;
+    width: 100%;
+    object-fit: cover;
+    border-radius: 8px 8px 0 0;
+    transition: transform 0.2s ease;
+    cursor: pointer;
+}
+
+.activity-photo:hover {
+    transform: scale(1.03);
+}
+
+.activity-title {
+    font-weight: 600;
+    font-size: 16px;
+    margin: 8px 0;
+    color: #1a1a1a;
+}
+
+.loading-container {
+    display: flex;
+    justify-content: center;
+    padding: 50px 0;
+}
\ No newline at end of file
diff --git a/src/components/ActivityBoard.jsx b/src/components/ActivityBoard.jsx
new file mode 100644
index 0000000..4cbfb52
--- /dev/null
+++ b/src/components/ActivityBoard.jsx
@@ -0,0 +1,98 @@
+// src/components/ActivityBoard.jsx
+import React, { useEffect, useState } from 'react';
+import { Card, Row, Col, Typography, Spin, message, Modal } from 'antd';
+import { getFullActivities } from '../api/activity';
+import './ActivityBoard.css';
+
+const { Paragraph, Text } = Typography;
+
+const ActivityBoard = () => {
+    const [activities, setActivities] = useState([]);
+    const [loading, setLoading] = useState(true);
+    const [modalVisible, setModalVisible] = useState(false);
+    const [selectedActivity, setSelectedActivity] = useState(null);
+
+    useEffect(() => {
+        getFullActivities()
+            .then((res) => {
+                setActivities(res.data);
+                setLoading(false);
+            })
+            .catch(() => {
+                message.error('获取公告失败');
+                setLoading(false);
+            });
+    }, []);
+
+    const showModal = (activity) => {
+        setSelectedActivity(activity);
+        setModalVisible(true);
+    };
+
+    const handleClose = () => {
+        setModalVisible(false);
+        setSelectedActivity(null);
+    };
+
+    return (
+        <>
+            {loading ? (
+                <div className="loading-container">
+                    <Spin size="large" />
+                </div>
+            ) : (
+                <Row gutter={[24, 24]}>
+                    {activities.map((activity) => (
+                        <Col xs={24} sm={12} md={8} lg={6} key={activity.activityid}>
+                            <Card
+                                hoverable
+                                className="activity-card"
+                                cover={
+                                    <img
+                                        alt="活动图片"
+                                        src={`http://localhost:8080${activity.photo}`}
+                                        className="activity-photo"
+                                        onClick={() => showModal(activity)}
+                                    />
+                                }
+                            >
+                                <Card.Meta
+                                    title={<div className="activity-title">{activity.title}</div>}
+                                />
+                            </Card>
+                        </Col>
+                    ))}
+                </Row>
+            )}
+
+            {/* 弹窗展示公告详情 */}
+            <Modal
+                open={modalVisible}
+                title={selectedActivity?.title}
+                footer={null}
+                onCancel={handleClose}
+                centered
+                width={700}
+                bodyStyle={{ padding: '24px', maxHeight: '80vh', overflowY: 'auto' }}
+            >
+                {selectedActivity && (
+                    <div>
+                        {selectedActivity.photo && (
+                            <img
+                                src={`http://localhost:8080${selectedActivity.photo}`}
+                                alt="公告图片"
+                                style={{ width: '100%', maxHeight: 400, objectFit: 'cover', borderRadius: 8, marginBottom: 20 }}
+                            />
+                        )}
+                        <Paragraph style={{ fontSize: 16 }}>{selectedActivity.content}</Paragraph>
+                        <Text type="secondary">
+                            发布时间:{new Date(selectedActivity.time).toLocaleString()}
+                        </Text>
+                    </div>
+                )}
+            </Modal>
+        </>
+    );
+};
+
+export default ActivityBoard;
diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx
new file mode 100644
index 0000000..f9af360
--- /dev/null
+++ b/src/pages/AdminPage.jsx
@@ -0,0 +1,135 @@
+import React, { useState } from 'react';
+import { Layout, Menu, Button } from 'antd';
+import {
+    FileTextOutlined,
+    FlagOutlined,
+    TagsOutlined,
+    DeploymentUnitOutlined,
+    HomeOutlined,
+    FileSearchOutlined
+} from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import PostAdminPanel from '../components/PostAdminPanel';
+import AdminActivityManager from '../components/AdminActivityManager';
+import ActivityAdminPanel from '../components/ActivityAdminPanel';
+import RequestAdminPanel from '../components/RequestAdminPanel';
+import ComplainAdminPanel from '../components/ComplainAdminPanel';
+import TorrentManagement from '../components/torrentmanage';
+import UserManagement from '../components/UserManagement';
+
+const { Header, Sider, Content, Footer } = Layout;
+
+const AdminPage = () => {
+    const navigate = useNavigate();
+    const [selectedKey, setSelectedKey] = useState('posts');
+
+    const renderContent = () => {
+        switch (selectedKey) {
+            case 'posts':
+                return <PostAdminPanel />;
+            case 'activities':
+                return <ActivityAdminPanel />;
+            case 'requests':
+                return <RequestAdminPanel />;
+            case 'complain':
+                return <ComplainAdminPanel />;
+            case 'torrent':
+                return <TorrentManagement/>;
+                 case 'user':
+                return <UserManagement/>;
+            default:
+                return null;
+        }
+    };
+
+    return (
+        <Layout style={{ minHeight: '100vh' }}>
+            {/* 侧边栏 */}
+            <Sider breakpoint="lg" collapsedWidth="0" theme="dark">
+                <div className="logo" style={{
+                    height: 64,
+                    display: 'flex',
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    fontSize: 22,
+                    color: 'white',
+                    fontWeight: 'bold',
+                }}>
+                    后台管理
+                </div>
+                <Menu
+                    theme="dark"
+                    mode="inline"
+                    selectedKeys={[selectedKey]}
+                    onClick={({ key }) => setSelectedKey(key)}
+                    items={[
+                        {
+                            key: 'posts',
+                            icon: <FileTextOutlined />,
+                            label: '帖子管理',
+                        },
+                        {
+                            key: 'activities',
+                            icon: <DeploymentUnitOutlined />,
+                            label: '公告管理',
+                        },
+                        {
+                            key: 'requests',
+                            icon: <FileSearchOutlined />,
+                            label: '求助帖管理',
+                        },
+                        {
+                            key: 'complain',
+                            icon: <FlagOutlined />,
+                            label: '举报管理',
+                        },
+                        {
+                            key: 'torrent',
+                            icon: <TagsOutlined />,
+                            label: '种子管理',
+                        },
+                                                {
+                            key: 'user',
+                            icon: <TagsOutlined />,
+                            label: '用户管理',
+                        },
+                    ]}
+                />
+                <div style={{ padding: 16 }}>
+                    <Button
+                        type="primary"
+                        block
+                        icon={<HomeOutlined />}
+                        onClick={() => navigate('/')}
+                    >
+                        返回首页
+                    </Button>
+                </div>
+            </Sider>
+
+            {/* 内容区 */}
+            <Layout>
+                <Header style={{ background: '#fff', padding: '0 24px', fontSize: 20 }}>
+                    {(() => {
+                        switch (selectedKey) {
+                            case 'posts': return '📄 帖子管理';
+                            case 'activities': return '🎯 公告管理';
+                            case 'requests': return '🆘 求助帖管理';
+                            case 'complain': return '🚨 举报管理';
+                            case 'torrent': return '🧲 种子管理';
+                            default: return '';
+                        }
+                    })()}
+                </Header>
+                <Content style={{ margin: '24px 16px 0', padding: 24, background: '#fff', minHeight: '85vh' }}>
+                    {renderContent()}
+                </Content>
+                <Footer style={{ textAlign: 'center' }}>
+                    PT管理系统 ©{new Date().getFullYear()} Created by Jiaxin Liu, Man Yang, Shuo Wang
+                </Footer>
+            </Layout>
+        </Layout>
+    );
+};
+
+export default AdminPage;