完成基本审核功能

Change-Id: Ib93823f864d5340b034a37af4e4cb3fb2cd5491a
diff --git a/TRM/front/src/components/Admin.js b/TRM/front/src/components/Admin.js
new file mode 100644
index 0000000..75253b8
--- /dev/null
+++ b/TRM/front/src/components/Admin.js
@@ -0,0 +1,272 @@
+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 = 3;
+  const [posts, setPosts] = useState([]);
+  const [loading, setLoading] = 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() {
+      const list = await fetchPosts(ADMIN_USER_ID)
+      setPosts(list)
+      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 }} />;
+
+  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/TRM/front/src/components/LogsDashboard.js b/TRM/front/src/components/LogsDashboard.js
new file mode 100644
index 0000000..1bd6cb7
--- /dev/null
+++ b/TRM/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/TRM/front/src/components/SuperAdmin.js b/TRM/front/src/components/SuperAdmin.js
new file mode 100644
index 0000000..118ab56
--- /dev/null
+++ b/TRM/front/src/components/SuperAdmin.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { NavLink, Outlet } from 'react-router-dom';
+import '../style/SuperAdmin.css';  // 可选:自定义样式
+
+export default function SuperAdmin() {
+  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/TRM/front/src/components/UserManagement.js b/TRM/front/src/components/UserManagement.js
new file mode 100644
index 0000000..400884b
--- /dev/null
+++ b/TRM/front/src/components/UserManagement.js
@@ -0,0 +1,44 @@
+import React, { useState, useEffect } from 'react';
+import '../style/Admin.css';
+
+function UserManagement() {
+  const [users, setUsers] = useState([]);
+
+  useEffect(() => {
+    fetch('/api/users')
+      .then(res => res.json())
+      .then(data => setUsers(data))
+      .catch(console.error);
+  }, []);
+
+  const handleUserAction = (id, action) => {
+    fetch(`/api/users/${id}/${action}`, { method: 'POST' })
+      .then(res => res.ok && setUsers(us => us.filter(u => u.id !== id)))
+      .catch(console.error);
+  };
+
+  return (
+    <div className="admin-container">
+      <h2>用户管理</h2>
+      <table className="admin-table">
+        <thead>
+          <tr><th>用户名</th><th>角色</th><th>操作</th></tr>
+        </thead>
+        <tbody>
+          {users.map(u => (
+            <tr key={u.id}>
+              <td>{u.username}</td>
+              <td>{u.role}</td>
+              <td>
+                <button onClick={() => handleUserAction(u.id, 'ban')}>封禁</button>
+                <button onClick={() => handleUserAction(u.id, 'promote')}>提升权限</button>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+export default UserManagement;