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;