feat: 完整集成JWLLL搜索推荐系统到Merge项目
新增功能:
- 完整的JWLLL搜索推荐后端服务 (back_jwlll/)
- 前端智能搜索和推荐功能集成
- HomeFeed组件增强: 数据源切换(原始数据 ↔ 智能推荐)
- 新增PostDetailJWLLL和UploadPageJWLLL组件
- 新增search_jwlll.js API接口
技术特性:
- 标签推荐和协同过滤推荐算法
- 中文分词和Word2Vec语义搜索
- 100%向后兼容,原功能完全保留
- 独立服务架构,无冲突部署
集成内容:
- JWLLL后端服务配置和依赖
- 前端路由和组件更新
- 样式文件和API集成
- 项目文档和启动工具
Change-Id: I1d008cf04eee40e7d81bfb9109f933d3447d1760
diff --git a/Merge/front/src/components/UploadPageJWLLL.jsx b/Merge/front/src/components/UploadPageJWLLL.jsx
new file mode 100644
index 0000000..2d9ee7d
--- /dev/null
+++ b/Merge/front/src/components/UploadPageJWLLL.jsx
@@ -0,0 +1,328 @@
+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>
+ )
+}