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