Revert "feat: 完整集成JWLLL搜索推荐系统到Merge项目"
This reverts commit b2ef519aa46c958768dba291676a9a4590d2b9ff.
Reason for revert: <错误的接口修改>
Change-Id: Ie3e7748c5331c509f45757792af5fe9e17172953
diff --git a/Merge/front/src/components/CreatePost.jsx b/Merge/front/src/components/CreatePost.jsx
index c11e247..1d2f306 100644
--- a/Merge/front/src/components/CreatePost.jsx
+++ b/Merge/front/src/components/CreatePost.jsx
@@ -13,8 +13,10 @@
const navigate = useNavigate()
const { postId } = useParams()
const isEdit = Boolean(postId)
+
// 步骤:新帖先上传,编辑则直接到 detail
const [step, setStep] = useState(isEdit ? 'detail' : 'upload')
+ const [files, setFiles] = useState([])
const [mediaUrls, setMediaUrls] = useState([])
// 表单字段
@@ -51,8 +53,10 @@
.catch(err => setError(err.message))
.finally(() => setLoading(false))
}, [isEdit, postId])
+
// 上传回调
const handleUploadComplete = async uploadedFiles => {
+ setFiles(uploadedFiles)
// TODO: 真正上传到服务器后替换为服务端 URL
const urls = await Promise.all(
uploadedFiles.map(f => URL.createObjectURL(f))
diff --git a/Merge/front/src/components/HomeFeed.jsx b/Merge/front/src/components/HomeFeed.jsx
index e32a2eb..c681858 100644
--- a/Merge/front/src/components/HomeFeed.jsx
+++ b/Merge/front/src/components/HomeFeed.jsx
@@ -1,10 +1,9 @@
// src/components/HomeFeed.jsx
-import React, { useState, useEffect, useCallback } from 'react'
+import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { ThumbsUp } from 'lucide-react'
import { fetchPosts, fetchPost } from '../api/posts_wzy'
-import { searchAPI } from '../api/search_jwlll'
import '../style/HomeFeed.css'
const categories = [
@@ -12,120 +11,15 @@
'职场','情感','家居','游戏','旅行','健身'
]
-const recommendModes = [
- { label: '标签推荐', value: 'tag' },
- { label: '协同过滤推荐', value: 'cf' }
-]
-
-const DEFAULT_USER_ID = '3' // 确保数据库有此用户
-const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
-
export default function HomeFeed() {
const navigate = useNavigate()
const [activeCat, setActiveCat] = useState('推荐')
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
- // JWLLL 搜索推荐相关状态
- const [search, setSearch] = useState('')
- const [recMode, setRecMode] = useState('tag')
- const [recCFNum, setRecCFNum] = useState(20)
- const [useSearchRecommend, setUseSearchRecommend] = useState(false) // 是否使用搜索推荐模式 // JWLLL 搜索推荐功能函数
-
- // JWLLL搜索推荐内容
- const fetchSearchContent = useCallback(async (keyword = '') => {
- setLoading(true)
- setError(null)
- try {
- const data = await searchAPI.search(keyword || activeCat, activeCat === '推荐' ? undefined : activeCat)
- const formattedItems = (data.results || []).map(item => ({
- id: item.id,
- title: item.title,
- author: item.author || '佚名',
- avatar: `https://i.pravatar.cc/40?img=${item.id}`,
- img: item.img || '',
- likes: item.heat || 0,
- content: item.content
- }))
- setItems(formattedItems)
- } catch (e) {
- console.error('搜索失败:', e)
- setError('搜索失败')
- setItems([])
- }
- setLoading(false)
- }, [activeCat])
-
- // 标签推荐
- const fetchTagRecommend = useCallback(async (tags) => {
- setLoading(true)
- setError(null)
- try {
- const data = await searchAPI.recommendByTags(DEFAULT_USER_ID, tags)
- const formattedItems = (data.recommendations || []).map(item => ({
- id: item.id,
- title: item.title,
- author: item.author || '佚名',
- avatar: `https://i.pravatar.cc/40?img=${item.id}`,
- img: item.img || '',
- likes: item.heat || 0,
- content: item.content
- }))
- setItems(formattedItems)
- } catch (e) {
- console.error('标签推荐失败:', e)
- setError('标签推荐失败')
- setItems([])
- }
- setLoading(false)
- }, [])
-
- // 协同过滤推荐
- const fetchCFRecommend = useCallback(async (topN = recCFNum) => {
- setLoading(true)
- setError(null)
- try {
- const data = await searchAPI.userBasedRecommend(DEFAULT_USER_ID, topN)
- const formattedItems = (data.recommendations || []).map(item => ({
- id: item.id,
- title: item.title,
- author: item.author || '佚名',
- avatar: `https://i.pravatar.cc/40?img=${item.id}`,
- img: item.img || '',
- likes: item.heat || 0,
- content: item.content
- }))
- setItems(formattedItems)
- } catch (e) {
- console.error('协同过滤推荐失败:', e)
- setError('协同过滤推荐失败')
- setItems([])
- }
- setLoading(false)
- }, [recCFNum])
-
- // 获取用户兴趣标签后再推荐
- const fetchUserTagsAndRecommend = useCallback(async () => {
- setLoading(true)
- setError(null)
- let tags = []
- try {
- const data = await searchAPI.getUserTags(DEFAULT_USER_ID)
- tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
- } catch {
- tags = DEFAULT_TAGS
- }
- if (recMode === 'tag') {
- await fetchTagRecommend(tags)
- } else {
- await fetchCFRecommend()
- }
- setLoading(false)
- }, [recMode, fetchTagRecommend, fetchCFRecommend])
useEffect(() => {
- // 原始数据加载函数
- const loadPosts = async () => {
+ async function loadPosts() {
try {
const list = await fetchPosts() // [{id, title, heat, created_at}, …]
// 为了拿到 media_urls 和 user_id,这里再拉详情
@@ -149,149 +43,25 @@
setLoading(false)
}
}
+ loadPosts()
+ }, [])
- // 根据模式选择加载方式
- if (activeCat === '推荐' && useSearchRecommend) {
- fetchUserTagsAndRecommend()
- } else {
- loadPosts()
- }
- }, [activeCat, useSearchRecommend, fetchUserTagsAndRecommend])
- // 切换推荐模式时的额外处理
- useEffect(() => {
- if (activeCat === '推荐' && useSearchRecommend) {
- fetchUserTagsAndRecommend()
- }
- // eslint-disable-next-line
- }, [recMode, fetchUserTagsAndRecommend])
-
- // 根据模式选择不同的加载方式
- const handleSearch = e => {
- e.preventDefault()
- if (useSearchRecommend) {
- fetchSearchContent(search)
- } else {
- // 切换到搜索推荐模式
- setUseSearchRecommend(true)
- fetchSearchContent(search)
- }
- }
-
- const handlePostClick = (postId) => {
- navigate(`/post/${postId}`)
- }
return (
<div className="home-feed">
- {/* 数据源切换 */}
- <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
- <span>数据源:</span>
- <div style={{display:'flex', gap:8}}>
- <button
- className={!useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
- onClick={() => {setUseSearchRecommend(false); setActiveCat('推荐')}}
- type="button"
- style={{
- borderRadius: 20,
- padding: '4px 18px',
- border: !useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
- background: !useSearchRecommend ? '#fff0f0' : '#fff',
- color: !useSearchRecommend ? '#e84c4a' : '#333',
- fontWeight: !useSearchRecommend ? 600 : 400,
- cursor: 'pointer',
- transition: 'all 0.2s',
- outline: 'none',
- }}
- >原始数据</button>
- <button
- className={useSearchRecommend ? 'rec-btn styled active' : 'rec-btn styled'}
- onClick={() => {setUseSearchRecommend(true); setActiveCat('推荐')}}
- type="button"
- style={{
- borderRadius: 20,
- padding: '4px 18px',
- border: useSearchRecommend ? '2px solid #e84c4a' : '1px solid #ccc',
- background: useSearchRecommend ? '#fff0f0' : '#fff',
- color: useSearchRecommend ? '#e84c4a' : '#333',
- fontWeight: useSearchRecommend ? 600 : 400,
- cursor: 'pointer',
- transition: 'all 0.2s',
- outline: 'none',
- }}
- >智能推荐</button>
- </div>
- </div>
-
- {/* 推荐模式切换,仅在推荐页显示且使用搜索推荐时 */}
- {activeCat === '推荐' && useSearchRecommend && (
- <div style={{marginBottom:12, display:'flex', alignItems:'center', gap:16}}>
- <span style={{marginRight:8}}>推荐模式:</span>
- <div style={{display:'flex', gap:8}}>
- {recommendModes.map(m => (
- <button
- key={m.value}
- className={recMode===m.value? 'rec-btn styled active':'rec-btn styled'}
- onClick={() => setRecMode(m.value)}
- type="button"
- style={{
- borderRadius: 20,
- padding: '4px 18px',
- border: recMode===m.value ? '2px solid #e84c4a' : '1px solid #ccc',
- background: recMode===m.value ? '#fff0f0' : '#fff',
- color: recMode===m.value ? '#e84c4a' : '#333',
- fontWeight: recMode===m.value ? 600 : 400,
- cursor: 'pointer',
- transition: 'all 0.2s',
- outline: 'none',
- }}
- >{m.label}</button>
- ))}
- </div>
- {/* 协同过滤推荐数量选择 */}
- {recMode === 'cf' && (
- <div style={{display:'flex',alignItems:'center',gap:4}}>
- <span>推荐数量:</span>
- <select value={recCFNum} onChange={e => { setRecCFNum(Number(e.target.value)); fetchCFRecommend(Number(e.target.value)) }} style={{padding:'2px 8px',borderRadius:6,border:'1px solid #ccc'}}>
- {[10, 20, 30, 50].map(n => <option key={n} value={n}>{n}</option>)}
- </select>
- </div>
- )}
- </div>
- )}
-
- {/* 搜索栏 */}
- <form className="feed-search" onSubmit={handleSearch} style={{marginBottom:16, display:'flex', gap:8, alignItems:'center'}}>
- <input
- type="text"
- className="search-input"
- placeholder="搜索内容/标题/标签"
- value={search}
- onChange={e => setSearch(e.target.value)}
- />
- <button type="submit" className="search-btn">搜索</button>
- </form>
-
{/* 顶部分类 */}
<nav className="feed-tabs">
{categories.map(cat => (
<button
key={cat}
className={cat === activeCat ? 'tab active' : 'tab'}
- onClick={() => {
- setActiveCat(cat);
- setSearch('');
- if (useSearchRecommend) {
- if (cat === '推荐') {
- fetchUserTagsAndRecommend()
- } else {
- fetchSearchContent()
- }
- }
- }}
+ onClick={() => setActiveCat(cat)}
>
{cat}
</button>
))}
- </nav> {/* 状态提示 */}
+ </nav>
+
+ {/* 状态提示 */}
{loading ? (
<div className="loading">加载中…</div>
) : error ? (
@@ -299,27 +69,22 @@
) : (
/* 瀑布流卡片区 */
<div className="feed-grid">
- {items.length === 0 ? (
- <div style={{padding:32, color:'#aaa'}}>暂无内容</div>
- ) : (
- items.map(item => (
- <div key={item.id} className="feed-card" onClick={() => handlePostClick(item.id)}>
- {item.img && <img className="card-img" src={item.img} alt={item.title} />}
- <h3 className="card-title">{item.title}</h3>
- {item.content && <div className="card-content">{item.content.slice(0, 60) || ''}</div>}
- <div className="card-footer">
- <div className="card-author">
- <img className="avatar" src={item.avatar} alt={item.author} />
- <span className="username">{item.author}</span>
- </div>
- <div className="card-likes">
- <ThumbsUp size={16} />
- <span className="likes-count">{item.likes}</span>
- </div>
+ {items.map(item => (
+ <div key={item.id} className="feed-card">
+ <img className="card-img" src={item.img} alt={item.title} />
+ <h3 className="card-title">{item.title}</h3>
+ <div className="card-footer">
+ <div className="card-author">
+ <img className="avatar" src={item.avatar} alt={item.author} />
+ <span className="username">{item.author}</span>
+ </div>
+ <div className="card-likes">
+ <ThumbsUp size={16} />
+ <span className="likes-count">{item.likes}</span>
</div>
</div>
- ))
- )}
+ </div>
+ ))}
</div>
)}
</div>
diff --git a/Merge/front/src/components/LogsDashboard.js b/Merge/front/src/components/LogsDashboard.js
index 6ab4746..22047e2 100644
--- a/Merge/front/src/components/LogsDashboard.js
+++ b/Merge/front/src/components/LogsDashboard.js
@@ -3,9 +3,7 @@
import '../style/Admin.css';
function LogsDashboard() {
- // eslint-disable-next-line no-unused-vars
const [logs, setLogs] = useState([]);
- // eslint-disable-next-line no-unused-vars
const [stats, setStats] = useState({});
useEffect(() => {
diff --git a/Merge/front/src/components/PostDetailJWLLL.jsx b/Merge/front/src/components/PostDetailJWLLL.jsx
deleted file mode 100644
index 0dc7289..0000000
--- a/Merge/front/src/components/PostDetailJWLLL.jsx
+++ /dev/null
@@ -1,322 +0,0 @@
-import React, { useState, useEffect } from 'react'
-import { useParams, useNavigate } from 'react-router-dom'
-import { ArrowLeft, ThumbsUp, MessageCircle, Share2, BookmarkPlus, Heart, Eye } from 'lucide-react'
-import { searchAPI } from '../api/search_jwlll'
-import '../style/PostDetail.css'
-
-export default function PostDetail() {
- const { id } = useParams()
- const navigate = useNavigate()
- const [post, setPost] = useState(null)
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
- const [liked, setLiked] = useState(false)
- const [bookmarked, setBookmarked] = useState(false)
- const [likeCount, setLikeCount] = useState(0)
- const [comments, setComments] = useState([])
- const [newComment, setNewComment] = useState('')
- const [showComments, setShowComments] = useState(false)
-
- const DEFAULT_USER_ID = '3' // 默认用户ID
-
- useEffect(() => {
- fetchPostDetail()
- fetchComments()
- }, [id])
-
- const fetchPostDetail = async () => {
- setLoading(true)
- setError(null)
- try {
- const data = await searchAPI.getPostDetail(id)
- setPost(data)
- setLikeCount(data.heat || 0)
- } catch (error) {
- console.error('获取帖子详情失败:', error)
- setError('帖子不存在或已被删除')
- } finally {
- setLoading(false)
- }
- }
-
- const fetchComments = async () => {
- try {
- const data = await searchAPI.getComments(id)
- setComments(data.comments || [])
- } catch (error) {
- console.error('获取评论失败:', error)
- }
- }
-
- const handleBack = () => {
- navigate(-1)
- }
-
- const handleLike = async () => {
- try {
- const newLiked = !liked
- if (newLiked) {
- await searchAPI.likePost(id, DEFAULT_USER_ID)
- } else {
- await searchAPI.unlikePost(id, DEFAULT_USER_ID)
- }
- setLiked(newLiked)
- setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
- } catch (error) {
- console.error('点赞失败:', error)
- // 回滚状态
- setLiked(!liked)
- setLikeCount(prev => liked ? prev + 1 : prev - 1)
- }
- }
-
- const handleBookmark = () => {
- setBookmarked(!bookmarked)
- // 实际项目中这里应该调用后端API保存收藏状态
- }
-
- const handleShare = () => {
- // 分享功能
- if (navigator.share) {
- navigator.share({
- title: post?.title,
- text: post?.content,
- url: window.location.href,
- })
- } else {
- // 复制链接到剪贴板
- navigator.clipboard.writeText(window.location.href)
- alert('链接已复制到剪贴板')
- }
- }
-
- const handleAddComment = async (e) => {
- e.preventDefault()
- if (!newComment.trim()) return
-
- try {
- await searchAPI.addComment(id, DEFAULT_USER_ID, newComment)
- setNewComment('')
- fetchComments() // 刷新评论列表
- } catch (error) {
- console.error('添加评论失败:', error)
- alert('评论失败,请重试')
- }
- }
-
- if (loading) {
- return (
- <div className="post-detail">
- <div className="loading-container">
- <div className="loading-spinner"></div>
- <p>加载中...</p>
- </div>
- </div>
- )
- }
-
- if (error) {
- return (
- <div className="post-detail">
- <div className="error-container">
- <h2>😔 出错了</h2>
- <p>{error}</p>
- <button onClick={handleBack} className="back-btn">
- <ArrowLeft size={20} />
- 返回
- </button>
- </div>
- </div>
- )
- }
-
- if (!post) {
- return (
- <div className="post-detail">
- <div className="error-container">
- <h2>😔 帖子不存在</h2>
- <p>该帖子可能已被删除或不存在</p>
- <button onClick={handleBack} className="back-btn">
- <ArrowLeft size={20} />
- 返回
- </button>
- </div>
- </div>
- )
- }
-
- return (
- <div className="post-detail">
- {/* 顶部导航栏 */}
- <header className="post-header">
- <button onClick={handleBack} className="back-btn">
- <ArrowLeft size={20} />
- 返回
- </button>
- <div className="header-actions">
- <button onClick={handleShare} className="action-btn">
- <Share2 size={20} />
- </button>
- <button
- onClick={handleBookmark}
- className={`action-btn ${bookmarked ? 'active' : ''}`}
- >
- <BookmarkPlus size={20} />
- </button>
- </div>
- </header>
-
- {/* 主要内容区 */}
- <main className="post-content">
- {/* 帖子标题 */}
- <h1 className="post-title">{post.title}</h1>
-
- {/* 作者信息和元数据 */}
- <div className="post-meta">
- <div className="author-info">
- <div className="avatar">
- {post.author ? post.author.charAt(0).toUpperCase() : 'U'}
- </div>
- <div className="author-details">
- <span className="author-name">{post.author || '匿名用户'}</span>
- <span className="post-date">
- {post.create_time ? new Date(post.create_time).toLocaleDateString('zh-CN') : '未知时间'}
- </span>
- </div>
- </div>
- <div className="post-stats">
- <span className="stat-item">
- <Eye size={16} />
- {post.views || 0}
- </span>
- <span className="stat-item">
- <Heart size={16} />
- {likeCount}
- </span>
- </div>
- </div>
-
- {/* 标签 */}
- {post.tags && post.tags.length > 0 && (
- <div className="post-tags">
- {post.tags.map((tag, index) => (
- <span key={index} className="tag">{tag}</span>
- ))}
- </div>
- )}
-
- {/* 帖子正文 */}
- <div className="post-body">
- <p>{post.content}</p>
- </div>
-
- {/* 类别信息 */}
- {(post.category || post.type) && (
- <div className="post-category">
- {post.category && (
- <>
- <span className="category-label">分类:</span>
- <span className="category-name">{post.category}</span>
- </>
- )}
- {post.type && (
- <>
- <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
- <span className="category-name">{post.type}</span>
- </>
- )}
- </div>
- )}
-
- {/* 评论区 */}
- <div className="comments-section">
- <div className="comments-header">
- <button
- onClick={() => setShowComments(!showComments)}
- className="comments-toggle"
- >
- <MessageCircle size={20} />
- 评论 ({comments.length})
- </button>
- </div>
-
- {showComments && (
- <div className="comments-content">
- {/* 添加评论 */}
- <form onSubmit={handleAddComment} className="comment-form">
- <textarea
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
- placeholder="写下你的评论..."
- className="comment-input"
- rows={3}
- />
- <button type="submit" className="comment-submit">
- 发布评论
- </button>
- </form>
-
- {/* 评论列表 */}
- <div className="comments-list">
- {comments.length === 0 ? (
- <p className="no-comments">暂无评论</p>
- ) : (
- comments.map((comment, index) => (
- <div key={index} className="comment-item">
- <div className="comment-author">
- <div className="comment-avatar">
- {comment.user_name ? comment.user_name.charAt(0).toUpperCase() : 'U'}
- </div>
- <span className="comment-name">{comment.user_name || '匿名用户'}</span>
- <span className="comment-time">
- {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
- </span>
- </div>
- <div className="comment-content">
- {comment.content}
- </div>
- </div>
- ))
- )}
- </div>
- </div>
- )}
- </div>
- </main>
-
- {/* 底部操作栏 */}
- <footer className="post-footer">
- <div className="action-bar">
- <button
- onClick={handleLike}
- className={`action-button ${liked ? 'liked' : ''}`}
- >
- <ThumbsUp size={20} />
- <span>{likeCount}</span>
- </button>
-
- <button
- onClick={() => setShowComments(!showComments)}
- className="action-button"
- >
- <MessageCircle size={20} />
- <span>评论</span>
- </button>
-
- <button onClick={handleShare} className="action-button">
- <Share2 size={20} />
- <span>分享</span>
- </button>
-
- <button
- onClick={handleBookmark}
- className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
- >
- <BookmarkPlus size={20} />
- <span>收藏</span>
- </button>
- </div>
- </footer>
- </div>
- )
-}
diff --git a/Merge/front/src/components/Sidebar.jsx b/Merge/front/src/components/Sidebar.jsx
index e35db63..92bc8f1 100644
--- a/Merge/front/src/components/Sidebar.jsx
+++ b/Merge/front/src/components/Sidebar.jsx
@@ -7,8 +7,6 @@
Activity,
Users,
ChevronDown,
- Search,
- Upload,
} from 'lucide-react'
import '../App.css'
@@ -26,7 +24,6 @@
{ id: 'fans', label: '粉丝数据', path: '/dashboard/fans' },
]
},
- { id: 'upload-jwlll', label: '智能发布', icon: Upload, path: '/upload-jwlll' },
// { id: 'activity', label: '活动中心', icon: Activity, path: '/activity' },
// { id: 'notes', label: '笔记灵感', icon: BookOpen, path: '/notes' },
// { id: 'creator', label: '创作学院', icon: Users, path: '/creator' },
diff --git a/Merge/front/src/components/UploadPageJWLLL.jsx b/Merge/front/src/components/UploadPageJWLLL.jsx
deleted file mode 100644
index 2d9ee7d..0000000
--- a/Merge/front/src/components/UploadPageJWLLL.jsx
+++ /dev/null
@@ -1,328 +0,0 @@
-import React, { useState } from 'react'
-import { Image, Video, Send } from 'lucide-react'
-import { searchAPI } from '../api/search_jwlll'
-import '../style/UploadPage.css'
-
-const categories = [
- '穿搭','美食','彩妆','影视',
- '职场','情感','家居','游戏','旅行','健身'
-]
-
-export default function UploadPageJWLLL({ onComplete }) {
- const [activeTab, setActiveTab] = useState('image')
- const [isDragOver, setIsDragOver] = useState(false)
- const [isUploading, setIsUploading] = useState(false)
- const [uploadedFiles, setUploadedFiles] = useState([])
- const [uploadProgress, setUploadProgress] = useState(0)
-
- // 新增表单字段
- const [title, setTitle] = useState('')
- const [content, setContent] = useState('')
- const [tags, setTags] = useState('')
- const [category, setCategory] = useState(categories[0])
- const [isPublishing, setIsPublishing] = useState(false)
-
- const DEFAULT_USER_ID = '3' // 默认用户ID
-
- const validateFiles = files => {
- const imgTypes = ['image/jpeg','image/jpg','image/png','image/webp']
- const vidTypes = ['video/mp4','video/mov','video/avi']
- const types = activeTab==='video'? vidTypes : imgTypes
- const max = activeTab==='video'? 2*1024*1024*1024 : 32*1024*1024
-
- const invalid = files.filter(f => !types.includes(f.type) || f.size > max)
- if (invalid.length) {
- alert(`发现 ${invalid.length} 个无效文件,请检查文件格式和大小`)
- return false
- }
- return true
- }
-
- const simulateUpload = files => {
- setIsUploading(true)
- setUploadProgress(0)
- setUploadedFiles(files)
- const iv = setInterval(() => {
- setUploadProgress(p => {
- if (p >= 100) {
- clearInterval(iv)
- setIsUploading(false)
- if (typeof onComplete === 'function') {
- onComplete(files)
- }
- return 100
- }
- return p + 10
- })
- }, 200)
- }
-
- const handleFileUpload = () => {
- if (isUploading) return
- const input = document.createElement('input')
- input.type = 'file'
- input.accept = activeTab==='video'? 'video/*' : 'image/*'
- input.multiple = activeTab==='image'
- input.onchange = e => {
- const files = Array.from(e.target.files)
- if (files.length > 0 && validateFiles(files)) simulateUpload(files)
- }
- input.click()
- }
-
- const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true) }
- const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false) }
- const handleDrop = e => {
- e.preventDefault(); e.stopPropagation(); setIsDragOver(false)
- if (isUploading) return
- const files = Array.from(e.dataTransfer.files)
- if (files.length > 0 && validateFiles(files)) simulateUpload(files)
- }
-
- const clearFiles = () => setUploadedFiles([])
- const removeFile = idx => setUploadedFiles(f => f.filter((_,i) => i!==idx))
-
- // 发布帖子
- const handlePublish = async () => {
- if (!title.trim()) {
- alert('请输入标题')
- return
- }
- if (!content.trim()) {
- alert('请输入内容')
- return
- }
-
- setIsPublishing(true)
- try {
- const postData = {
- user_id: DEFAULT_USER_ID,
- title: title.trim(),
- content: content.trim(),
- tags: tags.split(',').map(t => t.trim()).filter(t => t),
- category: category,
- type: activeTab === 'video' ? 'video' : 'image',
- media_files: uploadedFiles.map(f => f.name) // 实际项目中应该是上传后的URL
- }
-
- await searchAPI.uploadPost(postData)
- alert('发布成功!')
-
- // 清空表单
- setTitle('')
- setContent('')
- setTags('')
- setUploadedFiles([])
- setActiveTab('image')
-
- } catch (error) {
- console.error('发布失败:', error)
- alert('发布失败,请重试')
- } finally {
- setIsPublishing(false)
- }
- }
-
- return (
- <div className="upload-page-jwlll">
- <div className="upload-tabs">
- <button
- className={`upload-tab${activeTab==='video'?' active':''}`}
- onClick={() => setActiveTab('video')}
- >上传视频</button>
- <button
- className={`upload-tab${activeTab==='image'?' active':''}`}
- onClick={() => setActiveTab('image')}
- >上传图文</button>
- </div>
-
- {/* 内容表单 */}
- <div className="content-form">
- <div className="form-group">
- <label htmlFor="title">标题</label>
- <input
- id="title"
- type="text"
- value={title}
- onChange={(e) => setTitle(e.target.value)}
- placeholder="请输入标题..."
- className="form-input"
- maxLength={100}
- />
- </div>
-
- <div className="form-group">
- <label htmlFor="content">内容</label>
- <textarea
- id="content"
- value={content}
- onChange={(e) => setContent(e.target.value)}
- placeholder="请输入内容..."
- className="form-textarea"
- rows={4}
- maxLength={1000}
- />
- </div>
-
- <div className="form-row">
- <div className="form-group">
- <label htmlFor="category">分类</label>
- <select
- id="category"
- value={category}
- onChange={(e) => setCategory(e.target.value)}
- className="form-select"
- >
- {categories.map(cat => (
- <option key={cat} value={cat}>{cat}</option>
- ))}
- </select>
- </div>
-
- <div className="form-group">
- <label htmlFor="tags">标签</label>
- <input
- id="tags"
- type="text"
- value={tags}
- onChange={(e) => setTags(e.target.value)}
- placeholder="用逗号分隔多个标签..."
- className="form-input"
- />
- </div>
- </div>
- </div>
-
- {/* 文件上传区域 */}
- <div
- className={`upload-area${isDragOver?' drag-over':''}`}
- onDragOver={handleDragOver}
- onDragLeave={handleDragLeave}
- onDrop={handleDrop}
- >
- <div className="upload-icon">
- {activeTab==='video'? <Video/> : <Image/>}
- </div>
- <h2 className="upload-title">
- {activeTab==='video'
- ? '拖拽视频到此处或点击上传'
- : '拖拽图片到此处或点击上传'
- }
- </h2>
- <p className="upload-subtitle">(需支持上传格式)</p>
- <button
- className={`upload-btn${isUploading?' uploading':''}`}
- onClick={handleFileUpload}
- disabled={isUploading}
- >
- {isUploading
- ? `上传中... ${uploadProgress}%`
- : activeTab==='video'
- ? '上传视频'
- : '上传图片'
- }
- </button>
-
- {isUploading && (
- <div className="progress-container">
- <div className="progress-bar">
- <div
- className="progress-fill"
- style={{ width: `${uploadProgress}%` }}
- />
- </div>
- <div className="progress-text">{uploadProgress}%</div>
- </div>
- )}
- </div>
-
- {uploadedFiles.length > 0 && (
- <div className="file-preview-area">
- <div className="preview-header">
- <h3 className="preview-title">已上传文件 ({uploadedFiles.length})</h3>
- <button className="clear-files-btn" onClick={clearFiles}>
- 清除所有
- </button>
- </div>
- <div className="file-grid">
- {uploadedFiles.map((file, i) => (
- <div key={i} className="file-item">
- <button
- className="remove-file-btn"
- onClick={() => removeFile(i)}
- title="删除文件"
- >×</button>
- {file.type.startsWith('image/') ? (
- <div className="file-thumbnail">
- <img src={URL.createObjectURL(file)} alt={file.name} />
- </div>
- ) : (
- <div className="file-thumbnail video-thumbnail">
- <Video size={24} />
- </div>
- )}
- <div className="file-info">
- <div className="file-name" title={file.name}>
- {file.name.length > 20
- ? file.name.slice(0,17) + '...'
- : file.name
- }
- </div>
- <div className="file-size">
- {(file.size/1024/1024).toFixed(2)} MB
- </div>
- </div>
- </div>
- ))}
- </div>
- </div>
- )}
-
- {/* 发布按钮 */}
- <div className="publish-section">
- <button
- className={`publish-btn${isPublishing?' publishing':''}`}
- onClick={handlePublish}
- disabled={isPublishing || !title.trim() || !content.trim()}
- >
- <Send size={20} />
- {isPublishing ? '发布中...' : '发布'}
- </button>
- </div>
-
- <div className="upload-info fade-in">
- {activeTab==='image' ? (
- <>
- <div className="info-item">
- <h3 className="info-title">图片大小</h3>
- <p className="info-desc">最大32MB</p>
- </div>
- <div className="info-item">
- <h3 className="info-title">图片格式</h3>
- <p className="info-desc">png/jpg/jpeg/webp</p>
- </div>
- <div className="info-item">
- <h3 className="info-title">分辨率</h3>
- <p className="info-desc">建议720×960及以上</p>
- </div>
- </>
- ) : (
- <>
- <div className="info-item">
- <h3 className="info-title">视频大小</h3>
- <p className="info-desc">最大2GB,时长≤5分钟</p>
- </div>
- <div className="info-item">
- <h3 className="info-title">视频格式</h3>
- <p className="info-desc">mp4/mov</p>
- </div>
- <div className="info-item">
- <h3 className="info-title">分辨率</h3>
- <p className="info-desc">建议720P及以上</p>
- </div>
- </>
- )}
- </div>
- </div>
- )
-}