修改好友动态、发布动态、促销模块、创建帖子,Resolve review.
Change-Id: I84a2460dd1208bc703b0527d98225204d03e5efc
diff --git a/src/pages/Forum/posts-detail/PostDetailPage.css b/src/pages/Forum/posts-detail/PostDetailPage.css
index 65584af..780492b 100644
--- a/src/pages/Forum/posts-detail/PostDetailPage.css
+++ b/src/pages/Forum/posts-detail/PostDetailPage.css
@@ -1,7 +1,5 @@
.post-detail-page {
- background-color: #4b322b;
- min-height: 100vh;
- padding: 32px 0;
+ background: linear-gradient(180deg, #5F4437, #823c3c);
font-family: 'Helvetica Neue', sans-serif;
color: #333;
}
@@ -20,7 +18,7 @@
.post-detail h1 {
font-size: 24px;
margin-bottom: 16px;
- color: #4b322b;
+ color: #4A3B34;
}
.post-meta {
@@ -35,7 +33,7 @@
font-size: 20px;
line-height: 1.7;
margin-bottom: 20px;
- color: #4b322b;
+ color: #4A3B34;
}
/* 多图排列 */
@@ -60,7 +58,7 @@
border: none;
background: none;
cursor: pointer;
- color: #4b322b;
+ color: #4A3B34;
transition: transform 0.2s ease;
}
@@ -80,7 +78,7 @@
.comments-section h3 {
margin-bottom: 16px;
font-size: 20px;
- color: #4b322b;
+ color: #4A3B34;
background-color: #c4b3a3;
padding: 6px 12px;
border-radius: 8px;
@@ -168,7 +166,7 @@
}
.comment-options button:hover {
- background-color: #6d4e37;
+ background-color: #5F4437;
}
.error-text {
diff --git a/src/pages/Forum/posts-main/ForumPage.css b/src/pages/Forum/posts-main/ForumPage.css
index db3a654..8a7ae4c 100644
--- a/src/pages/Forum/posts-main/ForumPage.css
+++ b/src/pages/Forum/posts-main/ForumPage.css
@@ -1,6 +1,9 @@
.forum-page {
color: #fff;
- background-color: #5F4437;
+ /* background-color: #5F4437; */
+ /* background: linear-gradient(180deg, #5F4437, #9c737b); */
+ background: linear-gradient(180deg, #5F4437, #823c3c);
+ /* background-color: #5F4437; */
min-height: 100vh;
font-family: Arial, sans-serif;
}
@@ -97,8 +100,6 @@
.toolbar {
display: flex;
- /* justify-content: space-between;
- align-items: center; */
padding: 16px 20px;
}
diff --git a/src/pages/Forum/posts-main/ForumPage.jsx b/src/pages/Forum/posts-main/ForumPage.jsx
index 577d336..f3866ff 100644
--- a/src/pages/Forum/posts-main/ForumPage.jsx
+++ b/src/pages/Forum/posts-main/ForumPage.jsx
@@ -5,6 +5,7 @@
import CreatePostButton from './components/CreatePostButton';
import PostList from './components/PostList';
import './ForumPage.css';
+import Promotion from '../promotion-part/Promotion';
const ForumPage = () => {
const [searchQuery, setSearchQuery] = useState('');
@@ -16,6 +17,7 @@
return (
<div className="forum-page">
<Header />
+ <Promotion />
<div className="toolbar">
<CreatePostButton />
<SearchBar onSearch={handleSearch} />
diff --git a/src/pages/Forum/posts-main/components/CreatePostButton.css b/src/pages/Forum/posts-main/components/CreatePostButton.css
index b40b9e7..225ddde 100644
--- a/src/pages/Forum/posts-main/components/CreatePostButton.css
+++ b/src/pages/Forum/posts-main/components/CreatePostButton.css
@@ -1,19 +1,127 @@
.create-post {
- display: flex;
- justify-content: center;
- margin: 20px 0;
- }
-
- .create-btn {
- background-color: #BA929A;
- color: white;
- padding: 10px 20px;
- border-radius: 8px;
- border: none;
- cursor: pointer;
- font-size: 16px;
- transition: background-color 0.3s ease;
- }
+ display: flex;
+ justify-content: center;
+ margin: 20px 0;
+}
-
-
\ No newline at end of file
+.create-btn {
+ background-color: #BA929A;
+ color: white;
+ padding: 10px 20px;
+ border-radius: 8px;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ transition: background-color 0.3s ease;
+}
+
+.create-btn:hover {
+ background-color: #a17b83;
+}
+
+/* Modal 样式 */
+.cp-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.cp-modal-dialog {
+ background: #e9ded2;
+ padding: 20px;
+ width: 35%;
+ max-width: 500px;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+/* 标题 */
+.cp-modal-dialog h3 {
+ margin: 0;
+ color : #4A3B34;
+}
+
+/* 文本输入和文本域 —— 宽度保持 97%,一致感更强 */
+.cp-modal-dialog input[type="text"],
+.cp-modal-dialog textarea,
+.cp-modal-dialog input[type="file"] {
+ width: 97%;
+ padding: 8px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+/* 文本域高度 */
+.cp-modal-dialog textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+/* 文件选择按钮保持 label 方式,不变 */
+.file-label {
+ display: inline-block;
+ padding: 6px 10px;
+ background: #BA929A;
+ color: #fff;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ user-select: none;
+ width : 12%;
+}
+
+.file-label:hover {
+ background: #a17b83;
+}
+
+/* 预览区 */
+.cp-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.cp-preview img {
+ width: 80px;
+ height: 80px;
+ object-fit: cover;
+ border-radius: 4px;
+ border: 1px solid #bbb;
+}
+
+/* 按钮组 —— 同 .modal-actions */
+.cp-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 10px;
+}
+
+.cp-actions .btn {
+ padding: 6px 12px;
+ font-size: 14px;
+ cursor: pointer;
+ border: none;
+ border-radius: 4px;
+}
+
+.cp-actions .btn.cancel {
+ background: #5F4437;
+ color: #fff;
+}
+
+.cp-actions .btn.submit {
+ background: #BA929A;
+ color: #fff;
+}
diff --git a/src/pages/Forum/posts-main/components/CreatePostButton.jsx b/src/pages/Forum/posts-main/components/CreatePostButton.jsx
index 0632173..0390ee7 100644
--- a/src/pages/Forum/posts-main/components/CreatePostButton.jsx
+++ b/src/pages/Forum/posts-main/components/CreatePostButton.jsx
@@ -1,21 +1,131 @@
-import React from 'react';
-import { useLocation } from 'wouter';
+import React, { useState } from 'react';
+import axios from 'axios';
import { Edit } from '@icon-park/react';
import './CreatePostButton.css';
-const CreatePostButton = () => {
- const [, navigate] = useLocation();
+const API_BASE = process.env.REACT_APP_API_BASE;
+const USER_ID = 456;
- const goToCreatePost = () => {
- navigate('/forum/create-post');
+const CreatePostButton = () => {
+ const [showModal, setShowModal] = useState(false);
+
+ // 表单字段
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [previewUrls, setPreviewUrls] = useState([]);
+ const [imageUrls, setImageUrls] = useState([]);
+
+ // 处理文件选中:预览 & 上传
+ const handleImageChange = async (e) => {
+ const files = Array.from(e.target.files);
+ if (!files.length) return;
+ // 本地预览
+ setPreviewUrls(files.map(f => URL.createObjectURL(f)));
+
+ // 并发上传,假设 /upload 接口返回 { url: '...' }
+ try {
+ const uploaded = await Promise.all(
+ files.map(file => {
+ const fd = new FormData();
+ fd.append('file', file);
+ return axios.post(`${API_BASE}/upload`, fd, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ }).then(res => res.data.url);
+ })
+ );
+ setImageUrls(uploaded);
+ } catch (err) {
+ console.error('图片上传失败:', err);
+ alert('封面图上传失败,请重试');
+ }
+ };
+
+ // 提交发帖
+ const handleSubmit = async () => {
+ if (!title.trim() || !content.trim()) {
+ alert('标题和内容均为必填项');
+ return;
+ }
+
+ try {
+ await axios.post(
+ `${API_BASE}/echo/forum/posts/${USER_ID}/createPost`,
+ {
+ title: title.trim(),
+ post_content: content.trim(),
+ image_url: imageUrls
+ }
+ );
+ // 重置状态并关闭
+ setTitle('');
+ setContent('');
+ setPreviewUrls([]);
+ setImageUrls([]);
+ setShowModal(false);
+ alert('发帖成功');
+ // 如需刷新帖子列表,可在这里触发外部回调
+ } catch (err) {
+ console.error('发帖失败:', err.response?.data || err);
+ alert(err.response?.data?.error || '发帖失败,请稍后重试');
+ }
};
return (
- <div className="create-post">
- <button onClick={goToCreatePost} className="create-btn">
- <Edit theme="outline" size="18" /> 发帖
- </button>
- </div>
+ <>
+ <div className="create-post">
+ <button onClick={() => setShowModal(true)} className="create-btn">
+ <Edit theme="outline" size="18" style={{ marginRight: 6 }} />
+ 发帖
+ </button>
+ </div>
+
+ {showModal && (
+ <div className="cp-modal-overlay" onClick={() => setShowModal(false)}>
+ <div className="cp-modal-dialog" onClick={e => e.stopPropagation()}>
+ <h3>创建新帖子</h3>
+
+ <input
+ type="text"
+ placeholder="帖子标题"
+ value={title}
+ onChange={e => setTitle(e.target.value)}
+ />
+
+ <textarea
+ placeholder="正文内容"
+ value={content}
+ onChange={e => setContent(e.target.value)}
+ />
+
+ <label className="file-label">
+ 选择图片
+ <input
+ type="file"
+ accept="image/*"
+ multiple
+ onChange={handleImageChange}
+ style={{ display: 'none' }}
+ />
+ </label>
+
+ <div className="cp-preview">
+ {previewUrls.map((url, i) => (
+ <img key={i} src={url} alt={`封面预览 ${i}`} />
+ ))}
+ </div>
+
+ <div className="cp-actions">
+ <button className="btn cancel" onClick={() => setShowModal(false)}>
+ 取消
+ </button>
+ <button className="btn submit" onClick={handleSubmit}>
+ 发布
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ </>
);
};
diff --git a/src/pages/Forum/posts-main/components/SearchBar.css b/src/pages/Forum/posts-main/components/SearchBar.css
index 60ba679..2a8349d 100644
--- a/src/pages/Forum/posts-main/components/SearchBar.css
+++ b/src/pages/Forum/posts-main/components/SearchBar.css
@@ -11,7 +11,7 @@
margin-right: -3px;
background-color: #e9ded2;
/* 文字颜色 */
- color: #4b322b;
+ color: #4A3B34;
padding: 8px 10px; /* 控制按钮的高度和宽度 */
font-size: 15px; /* 控制文字大小 */
}
diff --git a/src/pages/Forum/posts-main/components/SearchBar.jsx b/src/pages/Forum/posts-main/components/SearchBar.jsx
index 52b873d..281d578 100644
--- a/src/pages/Forum/posts-main/components/SearchBar.jsx
+++ b/src/pages/Forum/posts-main/components/SearchBar.jsx
@@ -16,7 +16,7 @@
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
- placeholder="标题"
+ placeholder="输入要搜索的帖子"
className="search-input"
/>
<button onClick={handleSearch} className="search-btn">搜索</button>
diff --git a/src/pages/Forum/promotion-part/Promotion.css b/src/pages/Forum/promotion-part/Promotion.css
new file mode 100644
index 0000000..2232ba5
--- /dev/null
+++ b/src/pages/Forum/promotion-part/Promotion.css
@@ -0,0 +1,110 @@
+.promotion-container {
+ padding: 20px;
+ margin: 20px 0;
+ border-radius: 8px;
+}
+
+/* 并排两列 */
+.carousel-container {
+ display: flex;
+ gap: 20px;
+}
+
+.carousel-section {
+ flex: 1;
+}
+
+.carousel-section h2 {
+ font-size: 20px;
+ margin-bottom: 15px;
+}
+
+/* 轮播框架 */
+.carousel {
+ position: relative;
+ /* background: #a54747; */
+ /* background: linear-gradient(135deg, #4A3B34, #a54747); */
+ /* background: linear-gradient(135deg, #e38f77, #aa3e3e); */
+ /* 背景渐变 */
+ background: linear-gradient(135deg, #e1cab2, #b68791);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ padding: 15px;
+ color: #fff;
+ min-height: 200px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 左右箭头 */
+.carousel .arrow {
+ background: rgba(0,0,0,0.2);
+ border: none;
+ color: #fff;
+ font-size: 24px;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ cursor: pointer;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.carousel .arrow.left {
+ left: 10px;
+}
+
+.carousel .arrow.right {
+ right: 10px;
+}
+
+.carousel .arrow:hover {
+ background: rgba(0,0,0,0.4);
+}
+
+/* 每帧内容 */
+.carousel .slide {
+ width: calc(100% - 80px);
+ /* 留出箭头空间 */
+ text-align: left;
+}
+
+/* 冷门资源专用 slide */
+.cold-slide {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+/* 资源海报 */
+.resource-poster {
+ width: 80px;
+ height: 100px;
+ object-fit: cover;
+ border-radius: 4px;
+}
+
+/* 文本信息 */
+.resource-info div,
+.slide div {
+ margin-bottom: 6px;
+}
+
+/* 激励徽章 */
+.incentive-badge {
+ background-color: #17a2b8;
+ color: #d13c3c;
+ padding: 2px 8px;
+ margin-right: 6px;
+ font-size: 12px;
+ border-radius: 12px;
+}
+
+/* 空状态 */
+.empty-state {
+ color: #fff;
+ font-size: 16px;
+ text-align: center;
+}
diff --git a/src/pages/Forum/promotion-part/Promotion.jsx b/src/pages/Forum/promotion-part/Promotion.jsx
new file mode 100644
index 0000000..9af56bc
--- /dev/null
+++ b/src/pages/Forum/promotion-part/Promotion.jsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useState, useRef } from 'react';
+import './Promotion.css';
+
+const API_BASE = process.env.REACT_APP_API_BASE;
+
+const Promotion = () => {
+ const [promotions, setPromotions] = useState([]);
+ const [coldResources, setColdResources] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // 轮播索引
+ const [promoIndex, setPromoIndex] = useState(0);
+ const [coldIndex, setColdIndex] = useState(0);
+
+ // 计时器引用,用于清理
+ const promoTimerRef = useRef(null);
+ const coldTimerRef = useRef(null);
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ // 自动轮播:促销活动
+ useEffect(() => {
+ if (promotions.length === 0) return;
+ // 清理旧的计时器
+ clearInterval(promoTimerRef.current);
+ promoTimerRef.current = setInterval(() => {
+ setPromoIndex(prev => (prev + 1) % promotions.length);
+ }, 5000);
+ return () => clearInterval(promoTimerRef.current);
+ }, [promotions]);
+
+ // 自动轮播:冷门资源
+ useEffect(() => {
+ if (coldResources.length === 0) return;
+ clearInterval(coldTimerRef.current);
+ coldTimerRef.current = setInterval(() => {
+ setColdIndex(prev => (prev + 1) % coldResources.length);
+ }, 5000);
+ return () => clearInterval(coldTimerRef.current);
+ }, [coldResources]);
+
+ const fetchData = async () => {
+ try {
+ const promoResponse = await fetch(`${API_BASE}/echo/promotions/active`);
+ const promoData = await promoResponse.json();
+ setPromotions(promoData);
+
+ const coldResponse = await fetch(`${API_BASE}/echo/resources/cold`);
+ const coldData = await coldResponse.json();
+ setColdResources(coldData);
+ } catch (error) {
+ console.error('获取数据失败:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return <div className="promotion-container">加载中...</div>;
+ }
+
+ // 手动切换
+ const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
+ const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
+ const prevCold = () => setColdIndex((coldIndex - 1 + coldResources.length) % coldResources.length);
+ const nextCold = () => setColdIndex((coldIndex + 1) % coldResources.length);
+
+ const currentPromo = promotions[promoIndex];
+ const currentCold = coldResources[coldIndex];
+
+ return (
+ <div className="promotion-container carousel-container">
+ {/* 促销活动轮播 */}
+ <section className="carousel-section">
+ <h2>当前促销活动</h2>
+ {promotions.length === 0 ? (
+ <div className="empty-state">暂无促销活动</div>
+ ) : (
+ <div
+ className="carousel"
+ onMouseEnter={() => clearInterval(promoTimerRef.current)}
+ onMouseLeave={() => {
+ promoTimerRef.current = setInterval(() => {
+ setPromoIndex(prev => (prev + 1) % promotions.length);
+ }, 3000);
+ }}
+ >
+ <button className="arrow left" onClick={prevPromo}><</button>
+ <div className="slide">
+ <div><strong>种子类型:</strong>{currentPromo.category}</div>
+ <div><strong>促销时间:</strong>
+ {new Date(currentPromo.promotion_start_time).toLocaleString()} ~{' '}
+ {new Date(currentPromo.promotion_end_time).toLocaleString()}
+ </div>
+ <div><strong>下载折扣:</strong>{currentPromo.download_discount ?? '无'}</div>
+ <div><strong>上传奖励:</strong>{currentPromo.upload_reward ?? '无'}</div>
+ {currentPromo.description && (
+ <div><strong>详细描述:</strong>{currentPromo.description}</div>
+ )}
+ </div>
+ <button className="arrow right" onClick={nextPromo}>></button>
+ </div>
+ )}
+ </section>
+
+ {/* 冷门资源轮播 */}
+ <section className="carousel-section">
+ <h2>冷门资源推荐</h2>
+ {coldResources.length === 0 ? (
+ <div className="empty-state">暂无冷门资源推荐</div>
+ ) : (
+ <div
+ className="carousel"
+ onMouseEnter={() => clearInterval(coldTimerRef.current)}
+ onMouseLeave={() => {
+ coldTimerRef.current = setInterval(() => {
+ setColdIndex(prev => (prev + 1) % coldResources.length);
+ }, 3000);
+ }}
+ >
+ <button className="arrow left" onClick={prevCold}><</button>
+ <div className="slide cold-slide">
+ <img src={currentCold.poster} alt={currentCold.title} className="resource-poster" />
+ <div className="resource-info">
+ <div><strong>标题:</strong>{currentCold.title}</div>
+ <div><strong>下载量:</strong>{currentCold.download_count} | <strong>种子数:</strong>{currentCold.seed_count}</div>
+ <div><strong>激励:</strong>
+ {currentCold.incentives?.download_exempt && <span className="incentive-badge">免下载量</span>}
+ {currentCold.incentives?.extra_seed_bonus && <span className="incentive-badge">做种加成</span>}
+ {!(currentCold.incentives?.download_exempt || currentCold.incentives?.extra_seed_bonus) && '无'}
+ </div>
+ </div>
+ </div>
+ <button className="arrow right" onClick={nextCold}>></button>
+ </div>
+ )}
+ </section>
+ </div>
+ );
+};
+
+export default Promotion;