完成基本审核功能
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;