完成基本审核功能

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