合并JWL,WZY,TRM代码

Change-Id: Ifb4fcad3c06733e1e005e7d8d9403e3561010fb4
diff --git a/Merge/front/src/components/Admin.js b/Merge/front/src/components/Admin.js
new file mode 100644
index 0000000..da11100
--- /dev/null
+++ b/Merge/front/src/components/Admin.js
@@ -0,0 +1,283 @@
+import 'antd/dist/antd.css';
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { Layout, Tabs, Input, List, Card, Button, Tag, Spin, Typography, Divider } from 'antd';
+import '../style/Admin.css';
+import { fetchPosts, approvePost, rejectPost } from '../api/posts';
+
+export default function Admin() {
+  const ADMIN_USER_ID = 2;
+  const [posts, setPosts] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [hasPermission, setHasPermission] = useState(true);
+  const [activeTab, setActiveTab] = useState('all');
+  const [selectedPost, setSelectedPost] = useState(null);
+  const [searchTerm, setSearchTerm] = useState('');
+  
+  // 新增:拖拽相关状态
+  const [leftPanelWidth, setLeftPanelWidth] = useState(300);
+  const [isResizing, setIsResizing] = useState(false);
+
+  const statusColors = {
+    draft: 'orange',
+    pending: 'blue',
+    published: 'green',
+    deleted: 'gray',
+    rejected: 'red'
+  };
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const list = await fetchPosts(ADMIN_USER_ID)
+        setPosts(list)
+      } catch (e) {
+        if (e.message === 'Unauthorized') {
+          setHasPermission(false)
+        } else {
+          console.error(e)
+        }
+      } finally {
+        setLoading(false)
+      }
+    }
+    load()
+  }, [])
+
+  // 过滤并排序
+  const sortedPosts = useMemo(() => {
+    return [...posts].sort((a, b) => {
+      if (a.status === 'pending' && b.status !== 'pending') return -1
+      if (b.status === 'pending' && a.status !== 'pending') return 1
+      return 0
+    })
+  }, [posts])
+
+  // 调整:根据 activeTab 及搜索关键词过滤
+  const filteredPosts = useMemo(() => {
+    let list
+    switch (activeTab) {
+      case 'pending':
+        list = sortedPosts.filter(p => p.status === 'pending'); break
+      case 'published':
+        list = sortedPosts.filter(p => p.status === 'published'); break
+      case 'rejected':
+        list = sortedPosts.filter(p => p.status === 'rejected'); break
+      default:
+        list = sortedPosts
+    }
+    return list.filter(p =>
+      p.title.toLowerCase().includes(searchTerm.toLowerCase())
+    )
+  }, [sortedPosts, activeTab, searchTerm])
+
+  const handleApprove = async id => {
+    await approvePost(id, ADMIN_USER_ID)
+    setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'published' } : x))
+    // 同步更新选中的帖子状态
+    if (selectedPost?.id === id) {
+      setSelectedPost(prev => ({ ...prev, status: 'published' }));
+    }
+  }
+  const handleReject  = async id => {
+    await rejectPost(id, ADMIN_USER_ID)
+    setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'rejected' } : x))
+    // 同步更新选中的帖子状态
+    if (selectedPost?.id === id) {
+      setSelectedPost(prev => ({ ...prev, status: 'rejected' }));
+    }
+  }
+  const handleSelect  = post => setSelectedPost(post)
+
+  // 修复:拖拽处理函数
+  const handleMouseMove = useCallback((e) => {
+    if (!isResizing) return;
+    
+    const newWidth = e.clientX;
+    const minWidth = 200;
+    const maxWidth = window.innerWidth - 300;
+    
+    if (newWidth >= minWidth && newWidth <= maxWidth) {
+      setLeftPanelWidth(newWidth);
+    }
+  }, [isResizing]);
+
+  const handleMouseUp = useCallback(() => {
+    setIsResizing(false);
+    document.removeEventListener('mousemove', handleMouseMove);
+    document.removeEventListener('mouseup', handleMouseUp);
+    document.body.style.cursor = '';
+    document.body.style.userSelect = '';
+  }, [handleMouseMove]);
+
+  const handleMouseDown = useCallback((e) => {
+    e.preventDefault();
+    setIsResizing(true);
+    document.addEventListener('mousemove', handleMouseMove);
+    document.addEventListener('mouseup', handleMouseUp);
+    document.body.style.cursor = 'col-resize';
+    document.body.style.userSelect = 'none';
+  }, [handleMouseMove, handleMouseUp]);
+
+  // 新增:组件卸载时清理事件监听器
+  useEffect(() => {
+    return () => {
+      document.removeEventListener('mousemove', handleMouseMove);
+      document.removeEventListener('mouseup', handleMouseUp);
+      document.body.style.cursor = '';
+      document.body.style.userSelect = '';
+    };
+  }, [handleMouseMove, handleMouseUp]);
+
+  if (loading) return <Spin spinning tip="加载中…" style={{ width: '100%', marginTop: 100 }} />;
+  if (!hasPermission) return <div style={{ textAlign: 'center', marginTop: 100 }}>权限不足</div>;
+
+  const { Content } = Layout;
+  const { TabPane } = Tabs;
+  const { Title, Text } = Typography;
+
+  return (
+    <div style={{ height: '100vh', display: 'flex' }}>
+      {/* 左侧面板 */}
+      <div 
+        style={{ 
+          width: leftPanelWidth,
+          background: '#fff', 
+          padding: 16,
+          borderRight: '1px solid #f0f0f0',
+          overflow: 'hidden'
+        }}
+      >
+        <div style={{ marginBottom: 24 }}>
+          <Title level={3}>小红书</Title>
+          <Input.Search
+            placeholder="搜索帖子标题..."
+            value={searchTerm}
+            onChange={e => setSearchTerm(e.target.value)}
+            enterButton
+          />
+        </div>
+        <Tabs activeKey={activeTab} onChange={key => { setActiveTab(key); setSelectedPost(null); }}>
+          <TabPane tab="全部" key="all" />
+          <TabPane tab="待审核" key="pending" />
+          <TabPane tab="已通过" key="published" />
+          <TabPane tab="已驳回" key="rejected" />
+        </Tabs>
+        <div style={{ height: 'calc(100vh - 200px)', overflow: 'auto' }}>
+          <List
+            dataSource={filteredPosts}
+            pagination={{
+              pageSize: 5,
+              showSizeChanger: true,
+              pageSizeOptions: ['5','10','20'],
+              onChange: () => setSelectedPost(null)
+            }}
+            renderItem={p => (
+              <List.Item
+                key={p.id}
+                style={{
+                  background: selectedPost?.id === p.id ? '#e6f7ff' : '',
+                  cursor: 'pointer',
+                  marginBottom: 8
+                }}
+                onClick={() => handleSelect(p)}
+              >
+                <List.Item.Meta
+                  avatar={
+                    p.thumbnail && (
+                      <img
+                        src={p.thumbnail}
+                        alt=""
+                        style={{ width: 64, height: 64, objectFit: 'cover' }}
+                      />
+                    )
+                  }
+                  title={p.title}
+                  description={`${p.createdAt} · ${p.author} · ${p.likes || 0}赞`}
+                />
+                <Tag color={statusColors[p.status]}>{p.status}</Tag>
+              </List.Item>
+            )}
+          />
+        </div>
+      </div>
+
+      {/* 拖拽分割条 */}
+      <div
+        style={{
+          width: 5,
+          cursor: 'col-resize',
+          background: isResizing ? '#1890ff' : '#f0f0f0',
+          transition: isResizing ? 'none' : 'background-color 0.2s',
+          position: 'relative',
+          flexShrink: 0
+        }}
+        onMouseDown={handleMouseDown}
+        onSelectStart={(e) => e.preventDefault()}
+      >
+        <div
+          style={{
+            position: 'absolute',
+            top: '50%',
+            left: '50%',
+            transform: 'translate(-50%, -50%)',
+            width: 2,
+            height: 20,
+            background: '#999',
+            borderRadius: 1
+          }}
+        />
+      </div>
+
+      {/* 右侧内容区域 */}
+      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
+        <Content style={{ padding: 24, background: '#fff', overflow: 'auto' }}>
+          {selectedPost ? (
+            <Card
+              cover={selectedPost.image && <img alt="cover" src={selectedPost.image} />}
+              title={selectedPost.title}
+              extra={
+                <div>
+                  {selectedPost.status === 'pending' && (
+                    <>
+                      <Button type="primary" onClick={() => handleApprove(selectedPost.id)}>通过</Button>
+                      <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
+                    </>
+                  )}
+                  {selectedPost.status === 'published' && (
+                    <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
+                  )}
+                  {selectedPost.status === 'rejected' && (
+                    <>
+                      <Button onClick={() => {
+                        setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'pending' } : x));
+                        setSelectedPost(prev => ({ ...prev, status: 'pending' }));
+                      }}>恢复待审</Button>
+                      <Button onClick={() => {
+                        setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'published' } : x));
+                        setSelectedPost(prev => ({ ...prev, status: 'published' }));
+                      }}>恢复已发</Button>
+                    </>
+                  )}
+                </div>
+              }
+            >
+              <Text type="secondary">
+                {`${selectedPost.createdAt} · ${selectedPost.author} · ${selectedPost.likes || 0}赞`}
+              </Text>
+              <Divider />
+              <p>{selectedPost.content}</p>
+              <Divider />
+              <Title level={4}>合规性指引</Title>
+              <ul>
+                <li>不含违法违规内容</li>
+                <li>不侵害他人合法权益</li>
+              </ul>
+            </Card>
+          ) : (
+            <Text type="secondary">请选择左侧列表中的帖子查看详情</Text>
+          )}
+        </Content>
+      </div>
+    </div>
+  );
+}
diff --git a/Merge/front/src/components/CreatePost.jsx b/Merge/front/src/components/CreatePost.jsx
new file mode 100644
index 0000000..7519d5b
--- /dev/null
+++ b/Merge/front/src/components/CreatePost.jsx
@@ -0,0 +1,168 @@
+// src/components/CreatePost.jsx
+
+import React, { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import UploadPage from './UploadPage'
+import { createPost } from '../api/posts_wzy'
+import '../style/CreatePost.css'
+
+export default function CreatePost() {
+  const navigate = useNavigate()
+
+  const [step, setStep] = useState('upload')     // 'upload' | 'detail'
+  const [files, setFiles] = useState([])         // 本地 File 对象列表
+  const [mediaUrls, setMediaUrls] = useState([]) // 上传后得到的 URL 列表
+
+  // 详情表单字段
+  const [title, setTitle]     = useState('')
+  const [content, setContent] = useState('')
+  const [topicId, setTopicId] = useState('')
+  const [status, setStatus]   = useState('published')
+
+  const [error, setError]     = useState(null)
+
+  // 静态话题数据
+  const TOPICS = [
+    { id: 1, name: '世俱杯环球评大会' },
+    { id: 2, name: '我的REDmentor' },
+    { id: 3, name: '我染上了拼豆' },
+    // …更多静态话题…
+  ]
+
+  // 上传页面回调 —— 上传完成后切换到“填写详情”步骤
+  const handleUploadComplete = async uploadedFiles => {
+    setFiles(uploadedFiles)
+
+    // TODO: 改成真实上传逻辑,拿到真正的 media_urls
+    const urls = await Promise.all(
+      uploadedFiles.map(f => URL.createObjectURL(f))
+    )
+    setMediaUrls(urls)
+
+    setStep('detail')
+  }
+
+  // 发布按钮
+  const handleSubmit = async () => {
+    if (!title.trim() || !content.trim()) {
+      setError('标题和正文必填')
+      return
+    }
+    setError(null)
+    try {
+      await createPost({
+        user_id: 1,
+        topic_id: topicId || undefined,
+        title: title.trim(),
+        content: content.trim(),
+        media_urls: mediaUrls,
+        status
+      })
+      // 发布成功后跳转回首页
+      navigate('/home', { replace: true })
+    } catch (e) {
+      setError(e.message)
+    }
+  }
+
+  // 渲染上传页
+  if (step === 'upload') {
+    return <UploadPage onComplete={handleUploadComplete} />
+  }
+
+  // 渲染详情页
+  return (
+    <div className="create-post">
+      <h2>填写帖子内容</h2>
+      {error && <div className="error">{error}</div>}
+
+      {/* 已上传媒体预览 */}
+      <div className="preview-media">
+        {mediaUrls.map((url, i) => (
+          <div key={i} className="preview-item">
+            {files[i].type.startsWith('image/') ? (
+              <img src={url} alt={`预览 ${i}`} />
+            ) : (
+              <video src={url} controls />
+            )}
+          </div>
+        ))}
+      </div>
+
+      {/* 标题 */}
+      <label className="form-label">
+        标题(最多20字)
+        <input
+          type="text"
+          maxLength={20}
+          value={title}
+          onChange={e => setTitle(e.target.value)}
+          placeholder="填写标题会有更多赞哦~"
+        />
+        <span className="char-count">{title.length}/20</span>
+      </label>
+
+      {/* 正文 */}
+      <label className="form-label">
+        正文(最多1000字)
+        <textarea
+          maxLength={1000}
+          value={content}
+          onChange={e => setContent(e.target.value)}
+          placeholder="输入正文描述,真诚有价值的分享予人温暖"
+        />
+        <span className="char-count">{content.length}/1000</span>
+      </label>
+
+      {/* 话题选择 */}
+      <label className="form-label">
+        选择话题(可选)
+        <select
+          value={topicId}
+          onChange={e => setTopicId(e.target.value)}
+        >
+          <option value="">不添加话题</option>
+          {TOPICS.map(t => (
+            <option key={t.id} value={t.id}>
+              #{t.name}
+            </option>
+          ))}
+        </select>
+      </label>
+
+      {/* 发布状态 */}
+      <div className="status-group">
+        <label>
+          <input
+            type="radio"
+            name="status"
+            value="published"
+            checked={status === 'published'}
+            onChange={() => setStatus('published')}
+          />
+          立即发布
+        </label>
+        <label>
+          <input
+            type="radio"
+            name="status"
+            value="draft"
+            checked={status === 'draft'}
+            onChange={() => setStatus('draft')}
+          />
+          存为草稿
+        </label>
+      </div>
+
+      {/* 操作按钮 */}
+      <div className="btn-group">
+        <button className="btn btn-primary" onClick={handleSubmit}>
+          发布
+        </button>
+        <button className="btn btn-secondary" onClick={() => setStep('upload')}>
+          上一步
+        </button>
+      </div>
+    </div>
+  )
+}
diff --git a/Merge/front/src/components/Header.jsx b/Merge/front/src/components/Header.jsx
new file mode 100644
index 0000000..60a50b7
--- /dev/null
+++ b/Merge/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/Merge/front/src/components/HomeFeed.jsx b/Merge/front/src/components/HomeFeed.jsx
new file mode 100644
index 0000000..39e0ca8
--- /dev/null
+++ b/Merge/front/src/components/HomeFeed.jsx
@@ -0,0 +1,90 @@
+// src/components/HomeFeed.jsx
+
+import React, { useState, useEffect } from 'react'
+import { ThumbsUp } from 'lucide-react'
+import { fetchPosts, fetchPost } from '../api/posts_wzy'
+import '../style/HomeFeed.css'
+
+const categories = [
+  '推荐','穿搭','美食','彩妆','影视',
+  '职场','情感','家居','游戏','旅行','健身'
+]
+
+export default function HomeFeed() {
+  const [activeCat, setActiveCat] = useState('推荐')
+  const [items, setItems]         = useState([])
+  const [loading, setLoading]     = useState(true)
+  const [error, setError]         = useState(null)
+
+  useEffect(() => {
+    async function loadPosts() {
+      try {
+        const list = await fetchPosts()  // [{id, title, heat, created_at}, …]
+        // 为了拿到 media_urls 和 user_id,这里再拉详情
+        const detailed = await Promise.all(
+          list.map(async p => {
+            const d = await fetchPost(p.id)
+            return {
+              id:     d.id,
+              title:  d.title,
+              author: `作者 ${d.user_id}`,
+              avatar: `https://i.pravatar.cc/40?img=${d.user_id}`,
+              img:    d.media_urls?.[0] || '', // 用第一张媒体作为封面
+              likes:  d.heat
+            }
+          })
+        )
+        setItems(detailed)
+      } catch (e) {
+        setError(e.message)
+      } finally {
+        setLoading(false)
+      }
+    }
+    loadPosts()
+  }, [])
+
+  return (
+    <div className="home-feed">
+      {/* 顶部分类 */}
+      <nav className="feed-tabs">
+        {categories.map(cat => (
+          <button
+            key={cat}
+            className={cat === activeCat ? 'tab active' : 'tab'}
+            onClick={() => setActiveCat(cat)}
+          >
+            {cat}
+          </button>
+        ))}
+      </nav>
+
+      {/* 状态提示 */}
+      {loading ? (
+        <div className="loading">加载中…</div>
+      ) : error ? (
+        <div className="error">加载失败:{error}</div>
+      ) : (
+        /* 瀑布流卡片区 */
+        <div className="feed-grid">
+          {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
new file mode 100644
index 0000000..1bd6cb7
--- /dev/null
+++ b/Merge/front/src/components/LogsDashboard.js
@@ -0,0 +1,45 @@
+import React, { useEffect, useState } from 'react';
+import '../style/Admin.css';
+
+function LogsDashboard() {
+  const [logs, setLogs] = useState([]);
+  const [stats, setStats] = useState({});
+
+  useEffect(() => {
+    fetch('/api/logs')
+      .then(res => res.json())
+      .then(setLogs)
+      .catch(console.error);
+    fetch('/api/stats')
+      .then(res => res.json())
+      .then(setStats)
+      .catch(console.error);
+  }, []);
+
+  return (
+    <div className="admin-container">
+      <h2>运行日志 & 性能 Dashboard</h2>
+      <section className="dashboard-stats">
+        <pre>{JSON.stringify(stats, null, 2)}</pre>
+      </section>
+      <section className="dashboard-logs">
+        <table className="admin-table">
+          <thead>
+            <tr><th>时间</th><th>级别</th><th>消息</th></tr>
+          </thead>
+          <tbody>
+            {logs.map((log, i) => (
+              <tr key={i}>
+                <td>{new Date(log.time).toLocaleString()}</td>
+                <td>{log.level}</td>
+                <td>{log.message}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </section>
+    </div>
+  );
+}
+
+export default LogsDashboard;
diff --git a/Merge/front/src/components/PlaceholderPage.jsx b/Merge/front/src/components/PlaceholderPage.jsx
new file mode 100644
index 0000000..b290eb4
--- /dev/null
+++ b/Merge/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/Merge/front/src/components/Sidebar.jsx b/Merge/front/src/components/Sidebar.jsx
new file mode 100644
index 0000000..26118b2
--- /dev/null
+++ b/Merge/front/src/components/Sidebar.jsx
@@ -0,0 +1,103 @@
+import React, { useState, useEffect } from 'react'
+import { NavLink, useLocation, useNavigate } from 'react-router-dom'
+import {
+  Home,
+  BookOpen,
+  BarChart3,
+  Activity,
+  Users,
+  ChevronDown,
+} from 'lucide-react'
+import '../App.css'
+
+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()
+
+  // 打开 dashboard 下拉时保持展开
+  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"
+        onClick={() => navigate('/posts/new')}
+      >
+        发布笔记
+      </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>
+  )
+}
diff --git a/Merge/front/src/components/SuperAdmin.js b/Merge/front/src/components/SuperAdmin.js
new file mode 100644
index 0000000..817b708
--- /dev/null
+++ b/Merge/front/src/components/SuperAdmin.js
@@ -0,0 +1,64 @@
+import React, { useState, useEffect } from 'react';
+import { NavLink, Outlet } from 'react-router-dom';
+import { Spin } from 'antd';
+import { fetchUserList } from '../api/posts';
+import '../style/SuperAdmin.css';
+
+export default function SuperAdmin() {
+  const SUPERADMIN_USER_ID = 3;
+  const [loading, setLoading] = useState(true);
+  const [hasPermission, setHasPermission] = useState(true);
+
+  useEffect(() => {
+    async function check() {
+      try {
+        await fetchUserList(SUPERADMIN_USER_ID);
+      } catch (e) {
+        if (e.message === 'Unauthorized') {
+          setHasPermission(false);
+        } else {
+          console.error(e);
+        }
+      } finally {
+        setLoading(false);
+      }
+    }
+    check();
+  }, []);
+
+  if (loading) return <Spin spinning tip="加载中…" style={{ width: '100%', marginTop: 100 }} />;
+  if (!hasPermission) return <div style={{ textAlign: 'center', marginTop: 100 }}>权限不足</div>;
+
+  return (
+    <div className="super-admin-container">
+      <aside className="super-admin-sidebar">
+        <h2>超级管理员</h2>
+        <nav>
+          <ul>
+            <li>
+              <NavLink 
+                to="users" 
+                end 
+                className={({ isActive }) => isActive ? 'active' : ''}
+              >
+                用户管理
+              </NavLink>
+            </li>
+            <li>
+              <NavLink 
+                to="dashboard" 
+                className={({ isActive }) => isActive ? 'active' : ''}
+              >
+                平台运行监控
+              </NavLink>
+            </li>
+          </ul>
+        </nav>
+      </aside>
+
+      <main className="super-admin-content">
+        <Outlet />
+      </main>
+    </div>
+  );
+}
\ No newline at end of file
diff --git a/Merge/front/src/components/UploadPage.jsx b/Merge/front/src/components/UploadPage.jsx
new file mode 100644
index 0000000..817a210
--- /dev/null
+++ b/Merge/front/src/components/UploadPage.jsx
@@ -0,0 +1,230 @@
+// src/components/UploadPage.jsx
+
+import React, { useState } from 'react'
+import { Image, Video } from 'lucide-react'
+import '../style/UploadPage.css'
+
+
+/**
+ * @param {Object} props
+ * @param {(files: File[]) => void} [props.onComplete]  上传完成后回调,接收 File 数组
+ */
+export default function UploadPage({ 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 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} 个文件`)
+          // 上传完成后回调
+          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(prev => prev.filter((_, i) => i !== idx))
+
+  return (
+    <div className="upload-page">
+      {/* 上传类型切换 */}
+      <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 size={48} /> : <Image size={48} />}
+        </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>
+    </div>
+  )
+}
diff --git a/Merge/front/src/components/UserManagement.js b/Merge/front/src/components/UserManagement.js
new file mode 100644
index 0000000..4bd05c5
--- /dev/null
+++ b/Merge/front/src/components/UserManagement.js
@@ -0,0 +1,76 @@
+import React, { useState, useEffect } from 'react';
+import '../style/Admin.css';
+import { Select, message, Table } from 'antd';
+import { fetchUserList, giveUser, giveAdmin, giveSuperAdmin } from '../api/posts';
+
+const { Option } = Select;
+const ROLE_LIST = ['用户', '管理员', '超级管理员'];
+
+function UserManagement({ superAdminId }) {
+  const [users, setUsers] = useState([]);
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const data = superAdminId
+          ? await fetchUserList(superAdminId)
+          : await fetch('/api/users').then(res => res.json());
+        setUsers(data);
+      } catch (e) {
+        console.error(e);
+      }
+    }
+    load();
+  }, [superAdminId]);
+
+  // handle role changes
+  const handleRoleChange = async (userId, newRole) => {
+    try {
+      if (newRole === '用户') await giveUser(superAdminId, userId);
+      else if (newRole === '管理员') await giveAdmin(superAdminId, userId);
+      else if (newRole === '超级管理员') await giveSuperAdmin(superAdminId, userId);
+      setUsers(us => us.map(u => u.id === userId ? { ...u, role: newRole } : u));
+      message.success('修改成功');
+    } catch (e) {
+      console.error(e);
+      message.error('修改失败');
+    }
+  };
+
+  // define table columns
+  const columns = [
+    { title: '用户名', dataIndex: 'username', key: 'username' },
+    { title: '角色', dataIndex: 'role', key: 'role' },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_, record) => {
+        const orderedRoles = [record.role, ...ROLE_LIST.filter(r => r !== record.role)];
+        return (
+          <Select
+            value={record.role}
+            style={{ width: 120 }}
+            onChange={value => handleRoleChange(record.id, value)}
+          >
+            {orderedRoles.map(r => (
+              <Option key={r} value={r}>{r}</Option>
+            ))}
+          </Select>
+        );
+      },
+    },
+  ];
+
+  return (
+    <div className="admin-container">
+      <Table
+        dataSource={users}
+        columns={columns}
+        rowKey="id"
+        pagination={false}
+      />
+    </div>
+  );
+}
+
+export default UserManagement;