修改前端推荐搜素样式,链接推荐搜索前后端
Change-Id: I76d3be2a2cb6f553fc6c4512e11b6de3341ad0c4
diff --git a/JWLLL/API_front/src/App.css b/JWLLL/API_front/src/App.css
new file mode 100644
index 0000000..8fdd8d7
--- /dev/null
+++ b/JWLLL/API_front/src/App.css
@@ -0,0 +1,585 @@
+.app {
+ display: flex;
+ min-height: 100vh;
+ background-color: #f5f7fa;
+}
+
+/* Header */
+.header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 60px;
+ background: #fff;
+ border-bottom: 1px solid #e8eaed;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ z-index: 1000;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.logo {
+ background: #ff4757;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.header-title {
+ font-size: 18px;
+ font-weight: 500;
+ color: #333;
+}
+
+.header-right {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #666;
+ font-size: 14px;
+}
+
+/* Sidebar */
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 60px;
+ width: 200px;
+ height: calc(100vh - 60px);
+ background: #fff;
+ border-right: 1px solid #e8eaed;
+ overflow-y: auto;
+ z-index: 999;
+}
+
+.publish-btn {
+ margin: 16px;
+ background: #ff4757;
+ color: white;
+ padding: 10px 16px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+ transition: background 0.2s;
+}
+
+.publish-btn:hover {
+ background: #ff3742;
+}
+
+.nav-menu {
+ padding: 0;
+ list-style: none;
+}
+
+.nav-item {
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ padding: 12px 20px;
+ color: #333;
+ font-size: 14px;
+ transition: all 0.2s;
+ gap: 8px;
+}
+
+.nav-link:hover {
+ background: #f8f9fa;
+ color: #ff4757;
+}
+
+.nav-link.active {
+ background: linear-gradient(135deg, #ff4757, #ff6b7a);
+ color: white;
+ font-weight: 500;
+}
+
+.nav-link.active .lucide {
+ color: white;
+}
+
+.nav-submenu {
+ padding-left: 20px;
+ background: #fafafa;
+}
+
+.nav-submenu .nav-link {
+ padding: 8px 20px;
+ font-size: 13px;
+ color: #666;
+}
+
+.nav-submenu .nav-link:hover {
+ color: #ff4757;
+}
+
+/* Main Content */
+.main-content {
+ margin-left: 200px;
+ padding-top: 60px;
+ flex: 1;
+ min-height: 100vh;
+}
+
+.content-wrapper {
+ padding: 20px;
+ /* 原来是 max-width:1200px; */
+ max-width: none; /* 或者直接注释掉这一行 */
+ width: auto; /* 确保它能撑满父级 */
+ margin: 0; /* 取消水平 auto 居中 */
+}
+
+/* Upload Area */
+.upload-tabs {
+ display: flex;
+ gap: 20px;
+ margin-bottom: 30px;
+ border-bottom: 1px solid #e8eaed;
+}
+
+.upload-tab {
+ padding: 12px 0;
+ font-size: 16px;
+ color: #666;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s;
+}
+
+.upload-tab.active {
+ color: #ff4757;
+ border-bottom-color: #ff4757;
+ font-weight: 500;
+}
+
+.upload-area {
+ background: #fff;
+ border-radius: 8px;
+ padding: 80px 40px;
+ text-align: center;
+ border: 2px dashed #ddd;
+ margin-bottom: 40px;
+ transition: all 0.2s;
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+}
+
+.upload-area:hover {
+ border-color: #ff4757;
+ background: #fff8f8;
+}
+
+.upload-area.drag-over {
+ border-color: #ff4757;
+ background: #fff0f0;
+ transform: scale(1.02);
+}
+
+.upload-icon {
+ width: 100px;
+ height: 100px;
+ margin: 0 auto 30px;
+ background: #f8f9fa;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40px;
+ color: #ccc;
+ transition: all 0.3s ease;
+}
+
+.upload-area:hover .upload-icon {
+ background: #ff475710;
+ color: #ff4757;
+ transform: scale(1.1);
+}
+
+.upload-area.drag-over .upload-icon {
+ background: #ff475720;
+ color: #ff4757;
+ transform: scale(1.2);
+}
+
+.upload-title {
+ font-size: 20px;
+ color: #333;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.upload-subtitle {
+ font-size: 14px;
+ color: #999;
+ margin-bottom: 30px;
+}
+
+.upload-btn {
+ background: #ff4757;
+ color: white;
+ padding: 14px 28px;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 500;
+ transition: background 0.2s;
+ min-width: 120px;
+}
+
+.upload-btn:hover:not(:disabled) {
+ background: #ff3742;
+}
+
+.upload-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.upload-btn.uploading {
+ background: #ff4757;
+ opacity: 0.8;
+}
+
+/* File Preview */
+.file-preview-area {
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 40px;
+ border: 1px solid #e8eaed;
+}
+
+/* Preview Header */
+.preview-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.preview-title {
+ font-size: 16px;
+ color: #333;
+ margin-bottom: 16px;
+ font-weight: 500;
+}
+
+.clear-files-btn {
+ background: #ff4757;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ transition: background 0.2s;
+}
+
+.clear-files-btn:hover {
+ background: #ff3742;
+}
+
+.file-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 16px;
+}
+
+.file-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px;
+ border: 1px solid #f0f0f0;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.file-item:hover {
+ border-color: #ff4757;
+ box-shadow: 0 2px 8px rgba(255, 71, 87, 0.1);
+}
+
+.file-item:hover .remove-file-btn {
+ opacity: 1;
+}
+
+.remove-file-btn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ background: rgba(255, 71, 87, 0.8);
+ color: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: bold;
+ opacity: 0;
+ transition: all 0.2s;
+}
+
+.file-thumbnail {
+ width: 80px;
+ height: 80px;
+ border-radius: 6px;
+ overflow: hidden;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8f9fa;
+}
+
+.file-thumbnail img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.video-thumbnail {
+ color: #666;
+}
+
+.file-info {
+ text-align: center;
+ width: 100%;
+}
+
+.file-name {
+ font-size: 12px;
+ color: #333;
+ margin-bottom: 4px;
+ font-weight: 500;
+}
+
+.file-size {
+ font-size: 11px;
+ color: #999;
+}
+
+/* Upload Progress */
+.progress-container {
+ margin-top: 20px;
+ width: 100%;
+ max-width: 400px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 8px;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 8px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #ff4757, #ff6b7a);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ position: relative;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ animation: shimmer 1.5s infinite;
+}
+
+@keyframes shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.progress-text {
+ text-align: center;
+ font-size: 12px;
+ color: #666;
+ font-weight: 500;
+}
+
+/* Upload Info */
+.upload-info {
+ display: flex;
+ gap: 60px;
+ justify-content: center;
+ margin-top: 40px;
+ padding: 20px;
+ opacity: 1;
+ transition: opacity 0.3s ease;
+}
+
+.upload-info.fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.info-item {
+ text-align: center;
+ flex: 1;
+ max-width: 300px;
+}
+
+.info-title {
+ font-size: 16px;
+ color: #333;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.info-desc {
+ font-size: 13px;
+ color: #666;
+ line-height: 1.6;
+}
+
+/* Page Content Styles */
+.page-content {
+ padding: 40px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ margin: 20px 0;
+ min-height: 500px;
+}
+
+.page-header {
+ margin-bottom: 40px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #e8eaed;
+}
+
+.page-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #333;
+ margin: 0;
+}
+
+.page-body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+}
+
+.placeholder-content {
+ text-align: center;
+ max-width: 400px;
+}
+
+.placeholder-icon {
+ color: #ff4757;
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: center;
+}
+
+.placeholder-title {
+ font-size: 20px;
+ font-weight: 500;
+ color: #333;
+ margin: 0 0 15px 0;
+}
+
+.placeholder-desc {
+ font-size: 14px;
+ color: #666;
+ line-height: 1.6;
+ margin: 0;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ transition: transform 0.3s;
+ }
+
+ .main-content {
+ margin-left: 0;
+ }
+
+ .header-title {
+ display: none;
+ }
+
+ .upload-area {
+ padding: 60px 20px;
+ margin: 0 10px 30px;
+ }
+
+ .upload-info {
+ flex-direction: column;
+ gap: 30px;
+ padding: 10px;
+ }
+
+ .content-wrapper {
+ padding: 15px;
+ }
+
+ .upload-tabs {
+ gap: 15px;
+ }
+
+ .page-content {
+ padding: 20px;
+ margin: 10px;
+ }
+
+ .page-title {
+ font-size: 20px;
+ }
+
+ .placeholder-title {
+ font-size: 18px;
+ }
+
+ .placeholder-desc {
+ font-size: 13px;
+ }
+}
diff --git a/JWLLL/API_front/src/App.jsx b/JWLLL/API_front/src/App.jsx
new file mode 100644
index 0000000..9a79c5c
--- /dev/null
+++ b/JWLLL/API_front/src/App.jsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import { BrowserRouter as Router } from 'react-router-dom'
+import Header from './components/Header'
+import Sidebar from './components/Sidebar'
+import AppRouter from './router'
+
+import './App.css'
+
+export default function App() {
+ return (
+ <Router>
+ <div className="app">
+ <Header />
+ <Sidebar />
+ <main className="main-content">
+ <div className="content-wrapper">
+ <AppRouter />
+ </div>
+ </main>
+ </div>
+ </Router>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/components/Header.jsx b/JWLLL/API_front/src/components/Header.jsx
new file mode 100644
index 0000000..60a50b7
--- /dev/null
+++ b/JWLLL/API_front/src/components/Header.jsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { User } from 'lucide-react'
+import '../App.css' // 或者单独的 Header.css
+
+export default function Header() {
+ return (
+ <header className="header">
+ <div className="header-left">
+ <div className="logo">小红书</div>
+ <h1 className="header-title">创作服务平台</h1>
+ </div>
+ <div className="header-right">
+ <div className="user-info">
+ <User size={16} />
+ <span>小红薯63081EA1</span>
+ </div>
+ </div>
+ </header>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/components/HomeFeed.jsx b/JWLLL/API_front/src/components/HomeFeed.jsx
new file mode 100644
index 0000000..0b17b1f
--- /dev/null
+++ b/JWLLL/API_front/src/components/HomeFeed.jsx
@@ -0,0 +1,212 @@
+import React, { useState, useEffect } from 'react'
+import { ThumbsUp } from 'lucide-react'
+import '../style/HomeFeed.css'
+
+const categories = [
+ '推荐','穿搭','美食','彩妆','影视',
+ '职场','情感','家居','游戏','旅行','健身'
+]
+
+const recommendModes = [
+ { label: '标签推荐', value: 'tag' },
+ { label: '协同过滤推荐', value: 'cf' }
+]
+
+const DEFAULT_USER_ID = '3' // 确保数据库有此用户
+const DEFAULT_TAGS = ['美食','影视','穿搭'] // 可根据实际数据库调整
+
+export default function HomeFeed() {
+ const [activeCat, setActiveCat] = useState('推荐')
+ const [items, setItems] = useState([])
+ const [search, setSearch] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [recMode, setRecMode] = useState('tag')
+ const [userTags, setUserTags] = useState([])
+ const [recCFNum, setRecCFNum] = useState(20)
+
+ // 首次进入首页自动推荐内容
+ useEffect(() => {
+ if (activeCat === '推荐') {
+ fetchUserTagsAndRecommend()
+ } else {
+ fetchContent()
+ }
+ // eslint-disable-next-line
+ }, [])
+
+ // 切换推荐模式或分类时刷新内容
+ useEffect(() => {
+ if (activeCat === '推荐') {
+ fetchUserTagsAndRecommend()
+ } else {
+ fetchContent()
+ }
+ // eslint-disable-next-line
+ }, [activeCat, recMode])
+
+ // 获取用户兴趣标签后再推荐
+ const fetchUserTagsAndRecommend = async () => {
+ setLoading(true)
+ let tags = []
+ try {
+ const res = await fetch(`/user_tags?user_id=${DEFAULT_USER_ID}`)
+ const data = await res.json()
+ tags = Array.isArray(data.tags) && data.tags.length > 0 ? data.tags : DEFAULT_TAGS
+ setUserTags(tags)
+ } catch {
+ tags = DEFAULT_TAGS
+ setUserTags(tags)
+ }
+ if (recMode === 'tag') {
+ await fetchTagRecommend(tags)
+ } else {
+ await fetchCFRecommend()
+ }
+ setLoading(false)
+ }
+
+ const fetchContent = async (keyword = '') => {
+ setLoading(true)
+ try {
+ const res = await fetch('/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ keyword: keyword || activeCat, category: activeCat === '推荐' ? undefined : activeCat })
+ })
+ const data = await res.json()
+ setItems(data.results || [])
+ } catch (e) {
+ setItems([])
+ }
+ setLoading(false)
+ }
+
+ // 标签推荐
+ const fetchTagRecommend = async (tags) => {
+ setLoading(true)
+ try {
+ const res = await fetch('/recommend_tags', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user_id: DEFAULT_USER_ID, tags })
+ })
+ const data = await res.json()
+ setItems(data.recommendations || [])
+ } catch (e) {
+ setItems([])
+ }
+ setLoading(false)
+ }
+
+ // 协同过滤推荐
+ const fetchCFRecommend = async (topN = recCFNum) => {
+ setLoading(true)
+ try {
+ const res = await fetch('/user_based_recommend', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user_id: DEFAULT_USER_ID, top_n: topN })
+ })
+ const data = await res.json()
+ setItems(data.recommendations || [])
+ } catch (e) {
+ setItems([])
+ }
+ setLoading(false)
+ }
+
+ const handleSearch = e => {
+ e.preventDefault()
+ fetchContent(search)
+ }
+
+ return (
+ <div className="home-feed">
+ {/* 推荐模式切换,仅在推荐页显示 */}
+ {activeCat === '推荐' && (
+ <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('') }}
+ >
+ {cat}
+ </button>
+ ))}
+ </nav>
+ {/* 瀑布流卡片区 */}
+ <div className="feed-grid">
+ {loading ? <div style={{padding:32}}>加载中...</div> :
+ items.length === 0 ? <div style={{padding:32, color:'#aaa'}}>暂无推荐内容</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-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.heat || 0}</span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/components/PlaceholderPage.jsx b/JWLLL/API_front/src/components/PlaceholderPage.jsx
new file mode 100644
index 0000000..b290eb4
--- /dev/null
+++ b/JWLLL/API_front/src/components/PlaceholderPage.jsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import {
+ Home,
+ BookOpen,
+ Activity,
+ Users
+} from 'lucide-react'
+import '../App.css' // 或 PlaceholderPage.css
+
+const icons = {
+ home: Home,
+ notebooks: BookOpen,
+ activity: Activity,
+ notes: BookOpen,
+ creator: Users,
+ journal: BookOpen,
+}
+
+const titles = {
+ home: '欢迎来到小红书创作平台',
+ notebooks: '笔记管理功能开发中',
+ activity: '活动中心功能开发中',
+ notes: '笔记灵感功能开发中',
+ creator: '创作学院功能开发中',
+ journal: '创作日刊功能开发中',
+}
+
+const descs = {
+ home: '在这里您可以管理您的创作内容,查看数据分析,获取创作灵感。',
+ notebooks: '这里将显示您的所有笔记,支持编辑、删除、分类等操作。',
+ activity: '这里将展示最新的平台活动,让您参与更多有趣的创作活动。',
+ notes: '这里将为您提供创作灵感和写作建议,帮助您创作更好的内容。',
+ creator: '这里将提供创作技巧教学和平台规则说明,助您成为优秀创作者。',
+ journal: '这里将展示创作相关的最新资讯和平台动态。',
+}
+
+export default function PlaceholderPage({ pageId }) {
+ const Icon = icons[pageId] || Home
+ return (
+ <div className="page-content">
+ <div className="page-header">
+ <h1 className="page-title">{titles[pageId]}</h1>
+ </div>
+ <div className="page-body">
+ <div className="placeholder-content">
+ <div className="placeholder-icon">
+ <Icon size={48} />
+ </div>
+ <h3 className="placeholder-title">{titles[pageId]}</h3>
+ <p className="placeholder-desc">{descs[pageId]}</p>
+ </div>
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/components/Sidebar.jsx b/JWLLL/API_front/src/components/Sidebar.jsx
new file mode 100644
index 0000000..b6acad5
--- /dev/null
+++ b/JWLLL/API_front/src/components/Sidebar.jsx
@@ -0,0 +1,95 @@
+import React, { useState, useEffect } from 'react'
+import { NavLink, useLocation, useNavigate } from 'react-router-dom'
+import {
+ Home,
+ BookOpen,
+ BarChart3,
+ Activity,
+ Users,
+ ChevronDown,
+} from 'lucide-react'
+
+
+const menuItems = [
+ { id: 'home', label: '首页', icon: Home, path: '/home' },
+ { id: 'notebooks', label: '笔记管理', icon: BookOpen, path: '/notebooks' },
+ {
+ id: 'dashboard',
+ label: '数据看板',
+ icon: BarChart3,
+ path: '/dashboard',
+ submenu: [
+ { id: 'overview', label: '账号概况', path: '/dashboard/overview' },
+ { id: 'content', label: '内容分析', path: '/dashboard/content' },
+ { id: 'fans', label: '粉丝数据', path: '/dashboard/fans' },
+ ]
+ },
+ { id: 'activity', label: '活动中心', icon: Activity, path: '/activity' },
+ { id: 'notes', label: '笔记灵感', icon: BookOpen, path: '/notes' },
+ { id: 'creator', label: '创作学院', icon: Users, path: '/creator' },
+ { id: 'journal', label: '创作日刊', icon: BookOpen, path: '/journal' },
+]
+
+export default function Sidebar() {
+ const [expandedMenu, setExpandedMenu] = useState(null)
+ const location = useLocation()
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ if (location.pathname.startsWith('/dashboard')) {
+ setExpandedMenu('dashboard')
+ }
+ }, [location.pathname])
+
+ const toggleMenu = item => {
+ if (item.submenu) {
+ setExpandedMenu(expandedMenu === item.id ? null : item.id)
+ } else {
+ navigate(item.path)
+ setExpandedMenu(null)
+ }
+ }
+
+ return (
+ <aside className="sidebar">
+ <button className="publish-btn">发布笔记</button>
+ <nav className="nav-menu">
+ {menuItems.map(item => (
+ <div key={item.id} className="nav-item">
+ <a
+ href="#"
+ className={`nav-link${location.pathname === item.path ? ' active' : ''}`}
+ onClick={e => { e.preventDefault(); toggleMenu(item) }}
+ >
+ <item.icon size={16} />
+ <span>{item.label}</span>
+ {item.submenu && (
+ <ChevronDown
+ size={16}
+ style={{
+ marginLeft: 'auto',
+ transform: expandedMenu === item.id ? 'rotate(180deg)' : 'rotate(0deg)',
+ transition: 'transform 0.3s ease'
+ }}
+ />
+ )}
+ </a>
+ {item.submenu && expandedMenu === item.id && (
+ <div className="nav-submenu">
+ {item.submenu.map(sub => (
+ <NavLink
+ key={sub.id}
+ to={sub.path}
+ className={({ isActive }) => `nav-link${isActive ? ' active' : ''}`}
+ >
+ {sub.label}
+ </NavLink>
+ ))}
+ </div>
+ )}
+ </div>
+ ))}
+ </nav>
+ </aside>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/components/UploadPage.jsx b/JWLLL/API_front/src/components/UploadPage.jsx
new file mode 100644
index 0000000..e6db861
--- /dev/null
+++ b/JWLLL/API_front/src/components/UploadPage.jsx
@@ -0,0 +1,200 @@
+import React, { useState } from 'react'
+import { Image, Video } from 'lucide-react'
+
+
+export default function UploadPage() {
+ 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 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)
+ alert(`成功上传了 ${files.length} 个文件`)
+ 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))
+
+ return (
+ <>
+ <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={`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="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>
+ </>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/index.css b/JWLLL/API_front/src/index.css
new file mode 100644
index 0000000..72c144a
--- /dev/null
+++ b/JWLLL/API_front/src/index.css
@@ -0,0 +1,29 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f5f7fa;
+}
+
+button {
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-family: inherit;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+}
diff --git a/JWLLL/API_front/src/main.jsx b/JWLLL/API_front/src/main.jsx
new file mode 100644
index 0000000..b9a1a6d
--- /dev/null
+++ b/JWLLL/API_front/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+ <StrictMode>
+ <App />
+ </StrictMode>,
+)
diff --git a/JWLLL/API_front/src/router/index.jsx b/JWLLL/API_front/src/router/index.jsx
new file mode 100644
index 0000000..64191ac
--- /dev/null
+++ b/JWLLL/API_front/src/router/index.jsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import { Routes, Route, Navigate } from 'react-router-dom'
+import UploadPage from '../components/UploadPage'
+import PlaceholderPage from '../components/PlaceholderPage'
+import HomeFeed from '../components/HomeFeed'
+
+export default function AppRouter() {
+ return (
+ <Routes>
+ <Route path="/" element={<Navigate to="/dashboard" replace />} />
+
+ <Route path="/home" element={<HomeFeed />} />
+ <Route path="/notebooks" element={<PlaceholderPage pageId="notebooks" />} />
+ <Route path="/activity" element={<PlaceholderPage pageId="activity" />} />
+ <Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
+ <Route path="/creator" element={<PlaceholderPage pageId="creator" />} />
+ <Route path="/journal" element={<PlaceholderPage pageId="journal" />} />
+
+ <Route path="/dashboard/*" element={<UploadPage />} />
+
+ <Route path="*" element={<PlaceholderPage pageId="home" />} />
+ </Routes>
+ )
+}
\ No newline at end of file
diff --git a/JWLLL/API_front/src/style/HomeFeed.css b/JWLLL/API_front/src/style/HomeFeed.css
new file mode 100644
index 0000000..6cd353e
--- /dev/null
+++ b/JWLLL/API_front/src/style/HomeFeed.css
@@ -0,0 +1,153 @@
+/* --------- 容器 & Tabs --------- */
+.home-feed {
+ padding: 20px;
+}
+
+.feed-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.feed-tabs .tab {
+ padding: 6px 12px;
+ border: none;
+ background: #f0f0f0;
+ border-radius: 16px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.feed-tabs .tab.active {
+ background: #ff4757;
+ color: #fff;
+}
+
+/* --------- 瀑布流布局 --------- */
+.feed-grid {
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+}
+
+/* --------- 卡片样式及最大高度限制 --------- */
+.feed-card {
+ display: flex;
+ flex-direction: column;
+ max-height: 360px; /* 卡片最大高度 */
+ overflow: hidden; /* 超出部分隐藏 */
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.1);
+ transition: transform 0.2s;
+}
+
+.feed-card:hover {
+ transform: translateY(-4px);
+}
+
+/* 封面图固定高度 */
+.card-img {
+ width: 100%;
+ height: 180px; /* 固定图片区域高度 */
+ object-fit: cover;
+ flex-shrink: 0; /* 不随容器收缩 */
+}
+
+/* 标题填充剩余空间 */
+.card-title {
+ font-size: 14px;
+ color: #333;
+ margin: 12px;
+ line-height: 1.4;
+ flex: 1; /* 占满中间区域 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2; /* 最多两行 */
+ -webkit-box-orient: vertical;
+}
+
+/* --------- 底部:作者 + 点赞 --------- */
+.card-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ border-top: 1px solid #f0f0f0;
+ background: #fff;
+ flex-shrink: 0; /* 保持在底部 */
+}
+
+/* 作者区域 */
+.card-author {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.card-author .avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.card-author .username {
+ font-size: 13px;
+ color: #333;
+}
+
+/* 点赞区域 */
+.card-likes {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.card-likes svg {
+ color: #ff4757;
+}
+
+.card-likes .likes-count {
+ font-size: 13px;
+ color: #666;
+}
+
+/* 搜索框美化 */
+.feed-search {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+.search-input {
+ flex: 1;
+ padding: 8px 14px;
+ border: 1.5px solid #e0e0e0;
+ border-radius: 20px;
+ font-size: 15px;
+ outline: none;
+ transition: border 0.2s;
+ background: #fafbfc;
+}
+.search-input:focus {
+ border: 1.5px solid #e84c4a;
+ background: #fff;
+}
+.search-btn {
+ padding: 8px 22px;
+ border: none;
+ border-radius: 20px;
+ background: linear-gradient(90deg,#ff6a6a,#ff4757);
+ color: #fff;
+ font-weight: 600;
+ font-size: 15px;
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(255,71,87,0.08);
+ transition: background 0.2s, box-shadow 0.2s;
+}
+.search-btn:hover {
+ background: linear-gradient(90deg,#ff4757,#e84c4a);
+ box-shadow: 0 4px 16px rgba(255,71,87,0.15);
+}
\ No newline at end of file