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>
+  )
+}