blob: 6d278d58c7047bfcbfd1614e132889fe5b86e8a3 [file] [log] [blame]
TRM-coding78aa9662025-06-17 23:40:10 +08001import 'antd/dist/antd.css';
2import React, { useState, useEffect, useMemo, useCallback } from 'react';
3import { Layout, Tabs, Input, List, Card, Button, Tag, Spin, Typography, Divider } from 'antd';
4import '../style/Admin.css';
TRM-codingd1cbf672025-06-18 15:15:08 +08005import { fetchPosts, approvePost, rejectPost } from '../../../../Merge/front/src/api/posts';
TRM-coding78aa9662025-06-17 23:40:10 +08006
7export default function Admin() {
8 const ADMIN_USER_ID = 3;
9 const [posts, setPosts] = useState([]);
10 const [loading, setLoading] = useState(true);
11 const [activeTab, setActiveTab] = useState('all');
12 const [selectedPost, setSelectedPost] = useState(null);
13 const [searchTerm, setSearchTerm] = useState('');
14
15 // 新增:拖拽相关状态
16 const [leftPanelWidth, setLeftPanelWidth] = useState(300);
17 const [isResizing, setIsResizing] = useState(false);
18
19 const statusColors = {
20 draft: 'orange',
21 pending: 'blue',
22 published: 'green',
23 deleted: 'gray',
24 rejected: 'red'
25 };
26
27 useEffect(() => {
28 async function load() {
29 const list = await fetchPosts(ADMIN_USER_ID)
30 setPosts(list)
31 setLoading(false)
32 }
33 load()
34 }, [])
35
36 // 过滤并排序
37 const sortedPosts = useMemo(() => {
38 return [...posts].sort((a, b) => {
39 if (a.status === 'pending' && b.status !== 'pending') return -1
40 if (b.status === 'pending' && a.status !== 'pending') return 1
41 return 0
42 })
43 }, [posts])
44
45 // 调整:根据 activeTab 及搜索关键词过滤
46 const filteredPosts = useMemo(() => {
47 let list
48 switch (activeTab) {
49 case 'pending':
50 list = sortedPosts.filter(p => p.status === 'pending'); break
51 case 'published':
52 list = sortedPosts.filter(p => p.status === 'published'); break
53 case 'rejected':
54 list = sortedPosts.filter(p => p.status === 'rejected'); break
55 default:
56 list = sortedPosts
57 }
58 return list.filter(p =>
59 p.title.toLowerCase().includes(searchTerm.toLowerCase())
60 )
61 }, [sortedPosts, activeTab, searchTerm])
62
63 const handleApprove = async id => {
64 await approvePost(id, ADMIN_USER_ID)
65 setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'published' } : x))
66 // 同步更新选中的帖子状态
67 if (selectedPost?.id === id) {
68 setSelectedPost(prev => ({ ...prev, status: 'published' }));
69 }
70 }
71 const handleReject = async id => {
72 await rejectPost(id, ADMIN_USER_ID)
73 setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'rejected' } : x))
74 // 同步更新选中的帖子状态
75 if (selectedPost?.id === id) {
76 setSelectedPost(prev => ({ ...prev, status: 'rejected' }));
77 }
78 }
79 const handleSelect = post => setSelectedPost(post)
80
81 // 修复:拖拽处理函数
82 const handleMouseMove = useCallback((e) => {
83 if (!isResizing) return;
84
85 const newWidth = e.clientX;
86 const minWidth = 200;
87 const maxWidth = window.innerWidth - 300;
88
89 if (newWidth >= minWidth && newWidth <= maxWidth) {
90 setLeftPanelWidth(newWidth);
91 }
92 }, [isResizing]);
93
94 const handleMouseUp = useCallback(() => {
95 setIsResizing(false);
96 document.removeEventListener('mousemove', handleMouseMove);
97 document.removeEventListener('mouseup', handleMouseUp);
98 document.body.style.cursor = '';
99 document.body.style.userSelect = '';
100 }, [handleMouseMove]);
101
102 const handleMouseDown = useCallback((e) => {
103 e.preventDefault();
104 setIsResizing(true);
105 document.addEventListener('mousemove', handleMouseMove);
106 document.addEventListener('mouseup', handleMouseUp);
107 document.body.style.cursor = 'col-resize';
108 document.body.style.userSelect = 'none';
109 }, [handleMouseMove, handleMouseUp]);
110
111 // 新增:组件卸载时清理事件监听器
112 useEffect(() => {
113 return () => {
114 document.removeEventListener('mousemove', handleMouseMove);
115 document.removeEventListener('mouseup', handleMouseUp);
116 document.body.style.cursor = '';
117 document.body.style.userSelect = '';
118 };
119 }, [handleMouseMove, handleMouseUp]);
120
121 if (loading) return <Spin spinning tip="加载中…" style={{ width: '100%', marginTop: 100 }} />;
122
123 const { Content } = Layout;
124 const { TabPane } = Tabs;
125 const { Title, Text } = Typography;
126
127 return (
128 <div style={{ height: '100vh', display: 'flex' }}>
129 {/* 左侧面板 */}
130 <div
131 style={{
132 width: leftPanelWidth,
133 background: '#fff',
134 padding: 16,
135 borderRight: '1px solid #f0f0f0',
136 overflow: 'hidden'
137 }}
138 >
139 <div style={{ marginBottom: 24 }}>
140 <Title level={3}>小红书</Title>
141 <Input.Search
142 placeholder="搜索帖子标题..."
143 value={searchTerm}
144 onChange={e => setSearchTerm(e.target.value)}
145 enterButton
146 />
147 </div>
148 <Tabs activeKey={activeTab} onChange={key => { setActiveTab(key); setSelectedPost(null); }}>
149 <TabPane tab="全部" key="all" />
150 <TabPane tab="待审核" key="pending" />
151 <TabPane tab="已通过" key="published" />
152 <TabPane tab="已驳回" key="rejected" />
153 </Tabs>
154 <div style={{ height: 'calc(100vh - 200px)', overflow: 'auto' }}>
155 <List
156 dataSource={filteredPosts}
157 pagination={{
158 pageSize: 5,
159 showSizeChanger: true,
160 pageSizeOptions: ['5','10','20'],
161 onChange: () => setSelectedPost(null)
162 }}
163 renderItem={p => (
164 <List.Item
165 key={p.id}
166 style={{
167 background: selectedPost?.id === p.id ? '#e6f7ff' : '',
168 cursor: 'pointer',
169 marginBottom: 8
170 }}
171 onClick={() => handleSelect(p)}
172 >
173 <List.Item.Meta
174 avatar={
175 p.thumbnail && (
176 <img
177 src={p.thumbnail}
178 alt=""
179 style={{ width: 64, height: 64, objectFit: 'cover' }}
180 />
181 )
182 }
183 title={p.title}
184 description={`${p.createdAt} · ${p.author} · ${p.likes || 0}赞`}
185 />
186 <Tag color={statusColors[p.status]}>{p.status}</Tag>
187 </List.Item>
188 )}
189 />
190 </div>
191 </div>
192
193 {/* 拖拽分割条 */}
194 <div
195 style={{
196 width: 5,
197 cursor: 'col-resize',
198 background: isResizing ? '#1890ff' : '#f0f0f0',
199 transition: isResizing ? 'none' : 'background-color 0.2s',
200 position: 'relative',
201 flexShrink: 0
202 }}
203 onMouseDown={handleMouseDown}
204 onSelectStart={(e) => e.preventDefault()}
205 >
206 <div
207 style={{
208 position: 'absolute',
209 top: '50%',
210 left: '50%',
211 transform: 'translate(-50%, -50%)',
212 width: 2,
213 height: 20,
214 background: '#999',
215 borderRadius: 1
216 }}
217 />
218 </div>
219
220 {/* 右侧内容区域 */}
221 <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
222 <Content style={{ padding: 24, background: '#fff', overflow: 'auto' }}>
223 {selectedPost ? (
224 <Card
225 cover={selectedPost.image && <img alt="cover" src={selectedPost.image} />}
226 title={selectedPost.title}
227 extra={
228 <div>
229 {selectedPost.status === 'pending' && (
230 <>
231 <Button type="primary" onClick={() => handleApprove(selectedPost.id)}>通过</Button>
232 <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
233 </>
234 )}
235 {selectedPost.status === 'published' && (
236 <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
237 )}
238 {selectedPost.status === 'rejected' && (
239 <>
240 <Button onClick={() => {
241 setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'pending' } : x));
242 setSelectedPost(prev => ({ ...prev, status: 'pending' }));
243 }}>恢复待审</Button>
244 <Button onClick={() => {
245 setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'published' } : x));
246 setSelectedPost(prev => ({ ...prev, status: 'published' }));
247 }}>恢复已发</Button>
248 </>
249 )}
250 </div>
251 }
252 >
253 <Text type="secondary">
254 {`${selectedPost.createdAt} · ${selectedPost.author} · ${selectedPost.likes || 0}赞`}
255 </Text>
256 <Divider />
257 <p>{selectedPost.content}</p>
258 <Divider />
259 <Title level={4}>合规性指引</Title>
260 <ul>
261 <li>不含违法违规内容</li>
262 <li>不侵害他人合法权益</li>
263 </ul>
264 </Card>
265 ) : (
266 <Text type="secondary">请选择左侧列表中的帖子查看详情</Text>
267 )}
268 </Content>
269 </div>
270 </div>
271 );
272}