Merge "完成基本审核功能"
diff --git a/TRM/back/__pycache__/config.cpython-310.pyc b/TRM/back/__pycache__/config.cpython-310.pyc
index 02c50aa..47d55f3 100644
--- a/TRM/back/__pycache__/config.cpython-310.pyc
+++ b/TRM/back/__pycache__/config.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app.py b/TRM/back/app.py
index 5465905..3c7fb86 100644
--- a/TRM/back/app.py
+++ b/TRM/back/app.py
@@ -1,6 +1,8 @@
from app import create_app
+from flask_cors import CORS
app = create_app()
+CORS(app, resources={r"/*": {"origins": "*"}})
if __name__ == "__main__":
app.run(debug=True,port=5713,host='0.0.0.0')
\ No newline at end of file
diff --git a/TRM/back/app/__pycache__/__init__.cpython-310.pyc b/TRM/back/app/__pycache__/__init__.cpython-310.pyc
index 19d389d..f713fad 100644
--- a/TRM/back/app/__pycache__/__init__.cpython-310.pyc
+++ b/TRM/back/app/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/__pycache__/__init__.cpython-312.pyc b/TRM/back/app/__pycache__/__init__.cpython-312.pyc
index eaa8e71..ec28c7e 100644
--- a/TRM/back/app/__pycache__/__init__.cpython-312.pyc
+++ b/TRM/back/app/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/TRM/back/app/__pycache__/routes.cpython-310.pyc b/TRM/back/app/__pycache__/routes.cpython-310.pyc
index 3293666..e1c6837 100644
--- a/TRM/back/app/__pycache__/routes.cpython-310.pyc
+++ b/TRM/back/app/__pycache__/routes.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/functions/Fpost.py b/TRM/back/app/functions/Fpost.py
index 5651e8b..2f07b4a 100644
--- a/TRM/back/app/functions/Fpost.py
+++ b/TRM/back/app/functions/Fpost.py
@@ -1,6 +1,8 @@
from ..models.users import User as users
from ..models.post import Post as post
-
+import secrets
+import hashlib
+from datetime import datetime, timedelta
from sqlalchemy.orm import Session
class Fpost:
def __init__(self,session:Session):
@@ -23,9 +25,48 @@
return True
def review(self,postid,status):
+ print(status)
res=self.session.query(post).filter(post.id==postid).first()
if not res:
return False
res.status=status
self.session.commit()
- return True
\ No newline at end of file
+ return True
+
+ def createtoken(self, userid):
+ """
+ 根据userid创建token并插入到数据库
+ :param userid: 用户ID
+ :return: 生成的token字符串
+ """
+ # 生成随机盐值
+ salt = secrets.token_hex(16)
+
+ # 创建哈希值:userid + 当前时间戳 + 随机盐值
+ current_time = str(datetime.now().timestamp())
+ hash_input = f"{userid}_{current_time}_{salt}"
+
+ # 生成SHA256哈希值作为token
+ token = hashlib.sha256(hash_input.encode()).hexdigest()
+
+ # 设置时间
+ created_time = datetime.now()
+ expires_time = created_time + timedelta(days=1) # 一天后过期
+
+ try:
+ # 创建新的token记录
+ new_token = Token(
+ token=token,
+ expires_at=expires_time,
+ created_at=created_time
+ )
+
+ # 假设self.session是数据库会话对象
+ self.session.add(new_token)
+ self.session.commit()
+
+ return token
+
+ except Exception as e:
+ self.session.rollback()
+ raise Exception(f"创建token失败: {str(e)}")
\ No newline at end of file
diff --git a/TRM/back/app/functions/__pycache__/Fpost.cpython-310.pyc b/TRM/back/app/functions/__pycache__/Fpost.cpython-310.pyc
index fe0c6de..13abb35 100644
--- a/TRM/back/app/functions/__pycache__/Fpost.cpython-310.pyc
+++ b/TRM/back/app/functions/__pycache__/Fpost.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/models/__pycache__/__init__.cpython-310.pyc b/TRM/back/app/models/__pycache__/__init__.cpython-310.pyc
index f30dbeb..015de51 100644
--- a/TRM/back/app/models/__pycache__/__init__.cpython-310.pyc
+++ b/TRM/back/app/models/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/models/__pycache__/post.cpython-310.pyc b/TRM/back/app/models/__pycache__/post.cpython-310.pyc
index 263b592..8d33351 100644
--- a/TRM/back/app/models/__pycache__/post.cpython-310.pyc
+++ b/TRM/back/app/models/__pycache__/post.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/models/__pycache__/topics.cpython-310.pyc b/TRM/back/app/models/__pycache__/topics.cpython-310.pyc
index e291b93..fba569b 100644
--- a/TRM/back/app/models/__pycache__/topics.cpython-310.pyc
+++ b/TRM/back/app/models/__pycache__/topics.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/models/__pycache__/users.cpython-310.pyc b/TRM/back/app/models/__pycache__/users.cpython-310.pyc
index e6286c3..155a86c 100644
--- a/TRM/back/app/models/__pycache__/users.cpython-310.pyc
+++ b/TRM/back/app/models/__pycache__/users.cpython-310.pyc
Binary files differ
diff --git a/TRM/back/app/models/token.py b/TRM/back/app/models/token.py
new file mode 100644
index 0000000..cbe864b
--- /dev/null
+++ b/TRM/back/app/models/token.py
@@ -0,0 +1,27 @@
+from sqlalchemy import Column, Integer, String, DateTime, TIMESTAMP, Index
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.sql import func
+from datetime import datetime
+
+Base = declarative_base()
+
+class Token(Base):
+ __tablename__ = 'tokens'
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ token = Column(String(255), nullable=False, unique=True)
+ expires_at = Column(DateTime, nullable=False)
+ created_at = Column(TIMESTAMP, default=func.current_timestamp())
+ updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
+
+ __table_args__ = (
+ Index('idx_token', 'token'),
+ Index('idx_expires_at', 'expires_at'),
+ )
+
+ def __repr__(self):
+ return f"<Token(id={self.id}, token='{self.token[:10]}...', expires_at={self.expires_at})>"
+
+ def is_expired(self):
+ """检查token是否已过期"""
+ return datetime.now() > self.expires_at
\ No newline at end of file
diff --git a/TRM/back/app/routes.py b/TRM/back/app/routes.py
index 90c9c5c..19ff870 100644
--- a/TRM/back/app/routes.py
+++ b/TRM/back/app/routes.py
@@ -11,6 +11,7 @@
@main.route('/apostlist',methods=['POST','GET'])
def postlist():
data=request.get_json()
+ print(data)
engine=create_engine(Config.SQLURL)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
@@ -61,3 +62,19 @@
+@main.route('/nginxauth',methods=['POST','GET'])
+def nginxauth():
+ data=request.get_json()
+ engine=create_engine(Config.SQLURL)
+ SessionLocal = sessionmaker(bind=engine)
+ session = SessionLocal()
+ f=Fpost(session)
+ checres=f.checkid(data['userid'])
+ if(not checres):
+ return jsonify({'status': 'error', 'message': 'Unauthorized'})
+
+ res=f.nginxauth(data['postid'],data['status'])
+ if not res:
+ return jsonify({'status': 'error', 'message': 'Post not found'})
+
+ return jsonify({'status': 'success', 'message': 'Nginx auth updated successfully'})
\ No newline at end of file
diff --git a/TRM/front/package.json b/TRM/front/package.json
index 142eb20..78a6b75 100644
--- a/TRM/front/package.json
+++ b/TRM/front/package.json
@@ -11,7 +11,8 @@
"react-dom": "^19.1.0",
"react-router-dom": "^6.14.1",
"react-scripts": "^5.0.1",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "antd": "^4.24.0"
},
"scripts": {
"start": "react-scripts start",
diff --git a/TRM/front/src/Admin.css b/TRM/front/src/Admin.css
deleted file mode 100644
index 1697483..0000000
--- a/TRM/front/src/Admin.css
+++ /dev/null
@@ -1,65 +0,0 @@
-.admin-container {
- padding: 24px;
- background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-}
-
-.admin-title {
- font-size: 24px;
- color: #e61515;
- margin-bottom: 16px;
-}
-
-.admin-table {
- width: 100%;
- border-collapse: collapse;
-}
-
-.admin-table th,
-.admin-table td {
- border: 1px solid #f0f0f0;
- padding: 12px 16px;
- text-align: left;
-}
-
-.admin-table th {
- background-color: #fafafa;
- color: #333;
- font-weight: 500;
-}
-
-.status {
- font-weight: 500;
- text-transform: capitalize;
-}
-
-.status.pending {
- color: #f29900;
-}
-
-.status.approved {
- color: #28a745;
-}
-
-.status.banned {
- color: #d73a49;
-}
-
-.btn {
- padding: 6px 12px;
- margin-right: 8px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
-}
-
-.btn-approve {
- background-color: #e61515;
- color: #fff;
-}
-
-.btn-ban {
- background-color: #f5f5f5;
- color: #333;
-}
\ No newline at end of file
diff --git a/TRM/front/src/Admin.js b/TRM/front/src/Admin.js
deleted file mode 100644
index 1547706..0000000
--- a/TRM/front/src/Admin.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import './Admin.css';
-
-function AdminPage() {
- const [posts, setPosts] = useState([]);
-
- useEffect(() => {
- // TODO: 替换成你后端真正的接口
- fetch('/apostlist')
- .then(res => res.json())
- .then(data => setPosts(data))
- .catch(console.error);
- }, []);
-
- const handleAction = (id, action) => {
- // action: 'approve' | 'ban' | ...
- fetch(`/api/posts/${id}/${action}`, { method: 'POST' })
- .then(res => {
- if (res.ok) {
- // 简单地把该条移除或根据返回值更新状态
- setPosts(ps => ps.filter(p => p.id !== id));
- }
- })
- .catch(console.error);
- };
-
- return (
- <div className="admin-container">
- <h1 className="admin-title">小红书 · 管理员审核</h1>
- <table className="admin-table">
- <thead>
- <tr>
- <th>标题</th>
- <th>发布时间</th>
- <th>内容摘要</th>
- <th>状态</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- {posts.map(p => {
- const brief =
- p.content.length > 80
- ? p.content.slice(0, 80) + '...'
- : p.content;
- return (
- <tr key={p.id}>
- <td>{p.title}</td>
- <td>{new Date(p.createdAt).toLocaleString()}</td>
- <td>{brief}</td>
- <td className={`status ${p.status}`}>{p.status}</td>
- <td>
- <button
- className="btn btn-approve"
- onClick={() => handleAction(p.id, 'approve')}
- >
- 审核
- </button>
- <button
- className="btn btn-ban"
- onClick={() => handleAction(p.id, 'ban')}
- >
- 封禁
- </button>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- );
-}
-
-export default AdminPage;
\ No newline at end of file
diff --git a/TRM/front/src/api/posts.js b/TRM/front/src/api/posts.js
new file mode 100644
index 0000000..a42e781
--- /dev/null
+++ b/TRM/front/src/api/posts.js
@@ -0,0 +1,62 @@
+const BASE = 'http://10.126.59.25:5713' // 后端地址
+
+/**
+ * 获取待审核的帖子列表
+ * POST /apostlist
+ * @param {number|string} userId 平台管理员的用户 ID
+ * @returns Promise<[ {id, title, status}, … ]>
+ */
+export async function fetchPosts(userId) {
+ const res = await fetch(`${BASE}/apostlist`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId })
+ })
+ if (!res.ok) throw new Error(`fetchPosts: ${res.status}`)
+ return res.json()
+}
+
+/**
+ * 审核通过
+ * POST /areview
+ */
+export async function approvePost(postId, userId) {
+ const res = await fetch(`${BASE}/areview`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId, postid: postId, status: 'published' })
+ })
+ if (!res.ok) throw new Error(`approvePost: ${res.status}`)
+ return res.json()
+}
+
+/**
+ * 驳回
+ * POST /areview
+ */
+export async function rejectPost(postId, userId) {
+ const res = await fetch(`${BASE}/areview`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId, postid: postId, status: 'rejected' })
+ })
+ if (!res.ok) throw new Error(`rejectPost: ${res.status}`)
+ return res.json()
+}
+
+/**
+ * 获取单个帖子详情
+ * POST /agetpost
+ * @param {number|string} postId 帖子 ID
+ * @param {number|string} userId 平台管理员的用户 ID
+ * @returns Promise<{id, title, content, status}>
+ */
+export async function fetchPost(postId, userId) {
+ const res = await fetch(`${BASE}/agetpost`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId, postid: postId })
+ })
+ if (!res.ok) throw new Error(`fetchPost: ${res.status}`)
+ return res.json()
+}
\ No newline at end of file
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/LogsDashboard.js b/TRM/front/src/components/LogsDashboard.js
similarity index 97%
rename from TRM/front/src/LogsDashboard.js
rename to TRM/front/src/components/LogsDashboard.js
index c2e6239..1bd6cb7 100644
--- a/TRM/front/src/LogsDashboard.js
+++ b/TRM/front/src/components/LogsDashboard.js
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import './Admin.css';
+import '../style/Admin.css';
function LogsDashboard() {
const [logs, setLogs] = useState([]);
diff --git a/TRM/front/src/SuperAdmin.js b/TRM/front/src/components/SuperAdmin.js
similarity index 93%
rename from TRM/front/src/SuperAdmin.js
rename to TRM/front/src/components/SuperAdmin.js
index 0ddb9db..118ab56 100644
--- a/TRM/front/src/SuperAdmin.js
+++ b/TRM/front/src/components/SuperAdmin.js
@@ -1,6 +1,6 @@
import React from 'react';
import { NavLink, Outlet } from 'react-router-dom';
-import './SuperAdmin.css'; // 可选:自定义样式
+import '../style/SuperAdmin.css'; // 可选:自定义样式
export default function SuperAdmin() {
return (
diff --git a/TRM/front/src/UserManagement.js b/TRM/front/src/components/UserManagement.js
similarity index 97%
rename from TRM/front/src/UserManagement.js
rename to TRM/front/src/components/UserManagement.js
index ec7cbc2..400884b 100644
--- a/TRM/front/src/UserManagement.js
+++ b/TRM/front/src/components/UserManagement.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import './Admin.css';
+import '../style/Admin.css';
function UserManagement() {
const [users, setUsers] = useState([]);
diff --git a/TRM/front/src/index.js b/TRM/front/src/index.js
index 9c5a71b..fa7f2b6 100644
--- a/TRM/front/src/index.js
+++ b/TRM/front/src/index.js
@@ -1,8 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
-import './index.css';
-import App from './App';
+import './style/index.css';
+import App from './router/App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
diff --git a/TRM/front/src/logo.svg b/TRM/front/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/TRM/front/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
\ No newline at end of file
diff --git a/TRM/front/src/reportWebVitals.js b/TRM/front/src/reportWebVitals.js
deleted file mode 100644
index 5253d3a..0000000
--- a/TRM/front/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/TRM/front/src/App.js b/TRM/front/src/router/App.js
similarity index 75%
rename from TRM/front/src/App.js
rename to TRM/front/src/router/App.js
index 581dfe9..b9f9ac1 100644
--- a/TRM/front/src/App.js
+++ b/TRM/front/src/router/App.js
@@ -4,10 +4,10 @@
Route,
Navigate,
} from 'react-router-dom';
-import AdminPage from './Admin';
-import UserManagement from './UserManagement';
-import LogsDashboard from './LogsDashboard';
-import SuperAdmin from './SuperAdmin';
+import AdminPage from '../components/Admin';
+import UserManagement from '../components/UserManagement';
+import LogsDashboard from '../components/LogsDashboard';
+import SuperAdmin from '../components/SuperAdmin';
export default function App() {
return (
diff --git a/TRM/front/src/setupTests.js b/TRM/front/src/setupTests.js
deleted file mode 100644
index 8f2609b..0000000
--- a/TRM/front/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/TRM/front/src/style/Admin.css b/TRM/front/src/style/Admin.css
new file mode 100644
index 0000000..e5dff20
--- /dev/null
+++ b/TRM/front/src/style/Admin.css
@@ -0,0 +1,372 @@
+@import "~antd/dist/antd.css";
+
+/* 整体容器背景,弱化底层 */
+.admin-container {
+ background-color: #f5f6f8;
+}
+
+.admin-container {
+ padding: 24px;
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+/* 页眉分层:白底 + 圆角 + 阴影 */
+.page-header {
+ background: #fff;
+ padding: 12px 24px;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.admin-title {
+ font-size: 24px;
+ color: #e61515;
+ margin-bottom: 16px;
+}
+
+.admin-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.admin-table th,
+.admin-table td {
+ border: 1px solid #f0f0f0;
+ padding: 12px 16px;
+ text-align: left;
+}
+
+.admin-table th {
+ background-color: #fafafa;
+ color: #333;
+ font-weight: 500;
+}
+
+.status {
+ font-weight: 500;
+ text-transform: capitalize;
+}
+
+.status.pending {
+ color: #f29900;
+}
+
+.status.approved {
+ color: #28a745;
+}
+
+.status.banned {
+ color: #d73a49;
+}
+
+.btn {
+ padding: 6px 12px;
+ margin-right: 8px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.btn-approve {
+ background-color: #e61515;
+ color: #fff;
+}
+
+.btn-ban {
+ background-color: #f5f5f5;
+ color: #333;
+}
+
+/* 1. 瀑布流容器 */
+.admin-grid {
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ margin-top: 16px;
+}
+
+/* 2. 卡片 */
+.admin-card {
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.08);
+ overflow: hidden;
+ transition: transform 0.2s;
+}
+.admin-card:hover {
+ transform: translateY(-4px);
+}
+
+/* 3. 头部:用户名 + 状态 */
+.card-header {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f0f0f0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.card-header .username {
+ font-weight: 500;
+ color: #333;
+}
+.card-header .status {
+ font-weight: 500;
+ text-transform: capitalize;
+}
+.card-header .status.pending { color: #f29900; }
+.card-header .status.approved { color: #28a745; }
+.card-header .status.banned { color: #d73a49; }
+
+/* 4. 操作按钮区 */
+.card-actions {
+ display: flex;
+ padding: 12px 16px;
+ border-top: 1px solid #f0f0f0;
+ gap: 8px;
+}
+.card-actions .btn {
+ flex: 1;
+}
+.card-actions .btn-approve { background-color: #e61515; color: #fff; }
+.card-actions .btn-ban { background-color: #f5f5f5; color: #333; }
+
+/* —— Admin.js 专用布局 —— */
+.admin-layout {
+ display: flex;
+ gap: 16px;
+}
+
+/* 左侧列表区 */
+.list-panel {
+ width: 320px;
+ border-right: 1px solid #f0f0f0;
+ padding-right: 16px;
+ overflow-y: auto;
+ padding: 16px;
+}
+
+/* 顶部标签切换 */
+.tabs {
+ display: flex;
+ border-bottom: 1px solid #f0f0f0;
+ margin-bottom: 8px;
+ background: #fafafa;
+ padding: 0 16px;
+ border-radius: 8px 8px 0 0;
+}
+.tab-btn {
+ flex: 1;
+ padding: 8px 12px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ font-size: 14px;
+}
+.tab-btn.active {
+ border-color: #e61515;
+ color: #e61515;
+}
+
+/* 帖子列表 */
+.post-list {
+ /* 可根据需要添加滚动或间距 */
+}
+.post-item {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ cursor: pointer;
+ border-bottom: 1px solid #f5f5f5;
+ background: #fff;
+ margin-bottom: 4px;
+ border-radius: 4px;
+ transition: background 0.2s;
+}
+.post-item:hover {
+ background-color: #fafafa;
+}
+.post-item.selected {
+ background: #e6f1ff;
+}
+.thumb {
+ width: 40px;
+ height: 40px;
+ object-fit: cover;
+ border-radius: 4px;
+ margin-right: 8px;
+}
+.info {
+ flex: 1;
+}
+.info .title {
+ font-weight: 500;
+ color: #333;
+}
+.info .meta {
+ font-size: 12px;
+ color: #888;
+}
+
+/* 状态标签 */
+.status-tag {
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 12px;
+ text-transform: capitalize;
+}
+.status-tag.pending {
+ background-color: #fff4e5;
+ color: #f29900;
+}
+.status-tag.approved {
+ background-color: #e6f9f0;
+ color: #28a745;
+}
+.status-tag.rejected {
+ background-color: #fceaea;
+ color: #d73a49;
+}
+
+/* 右侧详情面板 */
+.detail-panel {
+ flex: 1;
+ padding-left: 16px;
+ max-height: calc(100vh - 100px);
+ overflow-y: auto;
+ padding: 24px;
+ margin-left: 8px;
+}
+
+/* 卡片阴影微调 */
+.admin-card {
+ box-shadow: 0 2px 6px rgba(0,0,0,0.08);
+}
+
+.detail-meta {
+ font-size: 12px;
+ color: #888;
+ margin-bottom: 8px;
+}
+.detail-content {
+ margin-bottom: 16px;
+ line-height: 1.6;
+}
+.detail-actions {
+ margin-bottom: 16px;
+ background: #f9f9fb;
+ padding: 12px;
+ border-radius: 4px;
+}
+
+/* 操作按钮 */
+.btn-reject {
+ background-color: #f5f5f5;
+ color: #333;
+}
+.rejected-label {
+ color: #d73a49;
+ font-weight: 500;
+}
+
+/* 加载与空状态 */
+.loading,
+.empty-state {
+ text-align: center;
+ padding: 16px;
+ color: #888;
+}
+
+/* 合规性指引 */
+.compliance-guidelines {
+ border-top: 1px solid #f0f0f0;
+ padding-top: 12px;
+ margin-top: 12px;
+ background: #f9f9fb;
+ padding: 12px;
+ border-radius: 4px;
+}
+.compliance-guidelines h4 {
+ margin-bottom: 8px;
+ font-size: 16px;
+}
+.compliance-guidelines ul {
+ padding-left: 20px;
+}
+.compliance-guidelines li {
+ line-height: 1.4;
+ margin-bottom: 4px;
+}
+
+/* 管理员导航栏样式 */
+.admin-nav {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ margin: 1rem 0 2rem;
+ border-bottom: 2px solid #e5e5e5;
+}
+
+.admin-nav button {
+ background: none;
+ border: none;
+ padding: 0.5rem 0;
+ font-size: 1rem;
+ color: #555;
+ cursor: pointer;
+ position: relative;
+ transition: color 0.3s ease;
+}
+
+.admin-nav button:hover {
+ color: #000;
+}
+
+.admin-nav button.active {
+ color: #0078d4;
+}
+
+.admin-nav button.active::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background-color: #0078d4;
+ border-radius: 2px 2px 0 0;
+}
+
+/* 页面头部:标题 + 搜索框 */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ background: #fff;
+ padding: 12px 24px;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+.main-title {
+ font-size: 28px;
+ color: #e61515;
+ margin: 0;
+}
+.search-input {
+ width: 240px;
+ padding: 6px 12px;
+ font-size: 14px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: border-color 0.2s;
+ background: #fafafa;
+}
+.search-input:focus {
+ outline: none;
+ border-color: #e61515;
+}
\ No newline at end of file
diff --git a/TRM/front/src/App.css b/TRM/front/src/style/App.css
similarity index 100%
rename from TRM/front/src/App.css
rename to TRM/front/src/style/App.css
diff --git a/TRM/front/src/SuperAdmin.css b/TRM/front/src/style/SuperAdmin.css
similarity index 100%
rename from TRM/front/src/SuperAdmin.css
rename to TRM/front/src/style/SuperAdmin.css
diff --git a/TRM/front/src/index.css b/TRM/front/src/style/index.css
similarity index 100%
rename from TRM/front/src/index.css
rename to TRM/front/src/style/index.css