blob: f73e239f98e9dd35d536cc03cda65d544c2554e7 [file] [log] [blame]
wu70fc8c52025-06-19 15:55:03 +08001// src/components/Admin.jsx
2import React, { useState, useEffect, useCallback } from 'react'
3import { useParams } from 'react-router-dom'
4import 'antd/dist/antd.css'
5import {
6 Layout,
7 Tabs,
8 Input,
9 List,
10 Card,
11 Button,
12 Tag,
13 Spin,
14 Typography,
15 Divider
16} from 'antd'
17import '../style/Admin.css'
18import { fetchPosts, approvePost, rejectPost } from '../api/posts_trm'
TRM-codingd1cbf672025-06-18 15:15:08 +080019
wu70fc8c52025-06-19 15:55:03 +080020export default function AdminPage() {
21 const { userId } = useParams() // ← 从路由拿到
22 const [posts, setPosts] = useState([])
23 const [loading, setLoading] = useState(true)
24 const [hasPermission, setHasPermission] = useState(true)
25 const [activeTab, setActiveTab] = useState('all')
26 const [selectedPost, setSelectedPost] = useState(null)
27 const [searchTerm, setSearchTerm] = useState('')
28 const [leftPanelWidth, setLeftPanelWidth] = useState(300)
29 const [isResizing, setIsResizing] = useState(false)
TRM-codingd1cbf672025-06-18 15:15:08 +080030
31 const statusColors = {
32 draft: 'orange',
33 pending: 'blue',
34 published: 'green',
35 deleted: 'gray',
36 rejected: 'red'
wu70fc8c52025-06-19 15:55:03 +080037 }
TRM-codingd1cbf672025-06-18 15:15:08 +080038
39 useEffect(() => {
40 async function load() {
41 try {
wu70fc8c52025-06-19 15:55:03 +080042 const list = await fetchPosts(userId) // ← 传入 userId
TRM-codingd1cbf672025-06-18 15:15:08 +080043 setPosts(list)
44 } catch (e) {
45 if (e.message === 'Unauthorized') {
46 setHasPermission(false)
47 } else {
48 console.error(e)
49 }
50 } finally {
51 setLoading(false)
52 }
53 }
54 load()
wu70fc8c52025-06-19 15:55:03 +080055 }, [userId])
TRM-codingd1cbf672025-06-18 15:15:08 +080056
wu70fc8c52025-06-19 15:55:03 +080057 const sortedPosts = React.useMemo(() => {
TRM-codingd1cbf672025-06-18 15:15:08 +080058 return [...posts].sort((a, b) => {
59 if (a.status === 'pending' && b.status !== 'pending') return -1
60 if (b.status === 'pending' && a.status !== 'pending') return 1
61 return 0
62 })
63 }, [posts])
64
wu70fc8c52025-06-19 15:55:03 +080065 const filteredPosts = React.useMemo(() => {
66 let list = sortedPosts
67 if (activeTab !== 'all') {
68 list = sortedPosts.filter(p => p.status === activeTab)
TRM-codingd1cbf672025-06-18 15:15:08 +080069 }
70 return list.filter(p =>
71 p.title.toLowerCase().includes(searchTerm.toLowerCase())
72 )
73 }, [sortedPosts, activeTab, searchTerm])
74
75 const handleApprove = async id => {
wu70fc8c52025-06-19 15:55:03 +080076 await approvePost(id, userId) // ← 传入 userId
77 setPosts(ps =>
78 ps.map(x => (x.id === id ? { ...x, status: 'published' } : x))
79 )
TRM-codingd1cbf672025-06-18 15:15:08 +080080 if (selectedPost?.id === id) {
wu70fc8c52025-06-19 15:55:03 +080081 setSelectedPost(prev => ({ ...prev, status: 'published' }))
TRM-codingd1cbf672025-06-18 15:15:08 +080082 }
83 }
TRM-codingd1cbf672025-06-18 15:15:08 +080084
wu70fc8c52025-06-19 15:55:03 +080085 const handleReject = async id => {
86 await rejectPost(id, userId) // ← 传入 userId
87 setPosts(ps =>
88 ps.map(x => (x.id === id ? { ...x, status: 'rejected' } : x))
89 )
90 if (selectedPost?.id === id) {
91 setSelectedPost(prev => ({ ...prev, status: 'rejected' }))
TRM-codingd1cbf672025-06-18 15:15:08 +080092 }
wu70fc8c52025-06-19 15:55:03 +080093 }
94
95 const handleSelect = post => setSelectedPost(post)
96
97 const handleMouseMove = useCallback(
98 e => {
99 if (!isResizing) return
100 const newWidth = e.clientX
101 const minWidth = 200
102 const maxWidth = window.innerWidth - 300
103 if (newWidth >= minWidth && newWidth <= maxWidth) {
104 setLeftPanelWidth(newWidth)
105 }
106 },
107 [isResizing]
108 )
TRM-codingd1cbf672025-06-18 15:15:08 +0800109
110 const handleMouseUp = useCallback(() => {
wu70fc8c52025-06-19 15:55:03 +0800111 setIsResizing(false)
112 document.removeEventListener('mousemove', handleMouseMove)
113 document.removeEventListener('mouseup', handleMouseUp)
114 document.body.style.cursor = ''
115 document.body.style.userSelect = ''
116 }, [handleMouseMove])
TRM-codingd1cbf672025-06-18 15:15:08 +0800117
wu70fc8c52025-06-19 15:55:03 +0800118 const handleMouseDown = useCallback(
119 e => {
120 e.preventDefault()
121 setIsResizing(true)
122 document.addEventListener('mousemove', handleMouseMove)
123 document.addEventListener('mouseup', handleMouseUp)
124 document.body.style.cursor = 'col-resize'
125 document.body.style.userSelect = 'none'
126 },
127 [handleMouseMove, handleMouseUp]
128 )
TRM-codingd1cbf672025-06-18 15:15:08 +0800129
TRM-codingd1cbf672025-06-18 15:15:08 +0800130 useEffect(() => {
131 return () => {
wu70fc8c52025-06-19 15:55:03 +0800132 document.removeEventListener('mousemove', handleMouseMove)
133 document.removeEventListener('mouseup', handleMouseUp)
134 document.body.style.cursor = ''
135 document.body.style.userSelect = ''
136 }
137 }, [handleMouseMove, handleMouseUp])
TRM-codingd1cbf672025-06-18 15:15:08 +0800138
wu70fc8c52025-06-19 15:55:03 +0800139 if (loading)
140 return (
141 <Spin
142 spinning
143 tip="加载中…"
144 style={{ width: '100%', marginTop: 100 }}
145 />
146 )
147 if (!hasPermission)
148 return (
149 <div style={{ textAlign: 'center', marginTop: 100 }}>
150 权限不足
151 </div>
152 )
TRM-codingd1cbf672025-06-18 15:15:08 +0800153
wu70fc8c52025-06-19 15:55:03 +0800154 const { Content } = Layout
155 const { TabPane } = Tabs
156 const { Title, Text } = Typography
TRM-codingd1cbf672025-06-18 15:15:08 +0800157
158 return (
159 <div style={{ height: '100vh', display: 'flex' }}>
wu70fc8c52025-06-19 15:55:03 +0800160 <div
161 style={{
TRM-codingd1cbf672025-06-18 15:15:08 +0800162 width: leftPanelWidth,
wu70fc8c52025-06-19 15:55:03 +0800163 background: '#fff',
TRM-codingd1cbf672025-06-18 15:15:08 +0800164 padding: 16,
165 borderRight: '1px solid #f0f0f0',
166 overflow: 'hidden'
167 }}
168 >
169 <div style={{ marginBottom: 24 }}>
wu70fc8c52025-06-19 15:55:03 +0800170 <Title level={3}>小红书 管理</Title>
TRM-codingd1cbf672025-06-18 15:15:08 +0800171 <Input.Search
172 placeholder="搜索帖子标题..."
173 value={searchTerm}
174 onChange={e => setSearchTerm(e.target.value)}
175 enterButton
176 />
177 </div>
wu70fc8c52025-06-19 15:55:03 +0800178 <Tabs
179 activeKey={activeTab}
180 onChange={key => {
181 setActiveTab(key)
182 setSelectedPost(null)
183 }}
184 >
TRM-codingd1cbf672025-06-18 15:15:08 +0800185 <TabPane tab="全部" key="all" />
186 <TabPane tab="待审核" key="pending" />
187 <TabPane tab="已通过" key="published" />
188 <TabPane tab="已驳回" key="rejected" />
189 </Tabs>
wu70fc8c52025-06-19 15:55:03 +0800190 <div
191 style={{
192 height: 'calc(100vh - 200px)',
193 overflow: 'auto'
194 }}
195 >
TRM-codingd1cbf672025-06-18 15:15:08 +0800196 <List
197 dataSource={filteredPosts}
198 pagination={{
199 pageSize: 5,
200 showSizeChanger: true,
wu70fc8c52025-06-19 15:55:03 +0800201 pageSizeOptions: ['5', '10', '20'],
TRM-codingd1cbf672025-06-18 15:15:08 +0800202 onChange: () => setSelectedPost(null)
203 }}
204 renderItem={p => (
205 <List.Item
206 key={p.id}
207 style={{
wu70fc8c52025-06-19 15:55:03 +0800208 background:
209 selectedPost?.id === p.id ? '#e6f7ff' : '',
TRM-codingd1cbf672025-06-18 15:15:08 +0800210 cursor: 'pointer',
211 marginBottom: 8
212 }}
213 onClick={() => handleSelect(p)}
214 >
215 <List.Item.Meta
216 avatar={
217 p.thumbnail && (
218 <img
219 src={p.thumbnail}
220 alt=""
wu70fc8c52025-06-19 15:55:03 +0800221 style={{
222 width: 64,
223 height: 64,
224 objectFit: 'cover'
225 }}
TRM-codingd1cbf672025-06-18 15:15:08 +0800226 />
227 )
228 }
229 title={p.title}
wu70fc8c52025-06-19 15:55:03 +0800230 description={`${p.createdAt} · ${p.author} · ${
231 p.likes || 0
232 }赞`}
TRM-codingd1cbf672025-06-18 15:15:08 +0800233 />
wu70fc8c52025-06-19 15:55:03 +0800234 <Tag color={statusColors[p.status]}>
235 {p.status}
236 </Tag>
TRM-codingd1cbf672025-06-18 15:15:08 +0800237 </List.Item>
238 )}
239 />
240 </div>
241 </div>
242
TRM-codingd1cbf672025-06-18 15:15:08 +0800243 <div
244 style={{
245 width: 5,
246 cursor: 'col-resize',
247 background: isResizing ? '#1890ff' : '#f0f0f0',
TRM-codingd1cbf672025-06-18 15:15:08 +0800248 position: 'relative',
249 flexShrink: 0
250 }}
251 onMouseDown={handleMouseDown}
wu70fc8c52025-06-19 15:55:03 +0800252 onSelectStart={e => e.preventDefault()}
TRM-codingd1cbf672025-06-18 15:15:08 +0800253 >
254 <div
255 style={{
256 position: 'absolute',
257 top: '50%',
258 left: '50%',
259 transform: 'translate(-50%, -50%)',
260 width: 2,
261 height: 20,
262 background: '#999',
263 borderRadius: 1
264 }}
265 />
266 </div>
267
wu70fc8c52025-06-19 15:55:03 +0800268 <div
269 style={{
270 flex: 1,
271 display: 'flex',
272 flexDirection: 'column'
273 }}
274 >
275 <Content
276 style={{
277 padding: 24,
278 background: '#fff',
279 overflow: 'auto'
280 }}
281 >
TRM-codingd1cbf672025-06-18 15:15:08 +0800282 {selectedPost ? (
283 <Card
wu70fc8c52025-06-19 15:55:03 +0800284 cover={
285 selectedPost.image && (
286 <img
287 alt="cover"
288 src={selectedPost.image}
289 />
290 )
291 }
TRM-codingd1cbf672025-06-18 15:15:08 +0800292 title={selectedPost.title}
293 extra={
294 <div>
295 {selectedPost.status === 'pending' && (
296 <>
wu70fc8c52025-06-19 15:55:03 +0800297 <Button
298 type="primary"
299 onClick={() =>
300 handleApprove(selectedPost.id)
301 }
302 >
303 通过
304 </Button>
305 <Button
306 danger
307 onClick={() =>
308 handleReject(selectedPost.id)
309 }
310 >
311 驳回
312 </Button>
TRM-codingd1cbf672025-06-18 15:15:08 +0800313 </>
314 )}
315 {selectedPost.status === 'published' && (
wu70fc8c52025-06-19 15:55:03 +0800316 <Button
317 danger
318 onClick={() =>
319 handleReject(selectedPost.id)
320 }
321 >
322 驳回
323 </Button>
TRM-codingd1cbf672025-06-18 15:15:08 +0800324 )}
325 {selectedPost.status === 'rejected' && (
326 <>
wu70fc8c52025-06-19 15:55:03 +0800327 <Button
328 onClick={() => {
329 setPosts(ps =>
330 ps.map(x =>
331 x.id === selectedPost.id
332 ? { ...x, status: 'pending' }
333 : x
334 )
335 )
336 setSelectedPost(prev => ({
337 ...prev,
338 status: 'pending'
339 }))
340 }}
341 >
342 恢复待审
343 </Button>
344 <Button
345 onClick={() => {
346 setPosts(ps =>
347 ps.map(x =>
348 x.id === selectedPost.id
349 ? {
350 ...x,
351 status: 'published'
352 }
353 : x
354 )
355 )
356 setSelectedPost(prev => ({
357 ...prev,
358 status: 'published'
359 }))
360 }}
361 >
362 恢复已发
363 </Button>
TRM-codingd1cbf672025-06-18 15:15:08 +0800364 </>
365 )}
366 </div>
367 }
368 >
369 <Text type="secondary">
wu70fc8c52025-06-19 15:55:03 +0800370 {`${selectedPost.createdAt} · ${selectedPost.author} · ${
371 selectedPost.likes || 0
372 }赞`}
TRM-codingd1cbf672025-06-18 15:15:08 +0800373 </Text>
374 <Divider />
375 <p>{selectedPost.content}</p>
376 <Divider />
377 <Title level={4}>合规性指引</Title>
378 <ul>
379 <li>不含违法违规内容</li>
380 <li>不侵害他人合法权益</li>
381 </ul>
382 </Card>
383 ) : (
wu70fc8c52025-06-19 15:55:03 +0800384 <Text type="secondary">
385 请选择左侧列表中的帖子查看详情
386 </Text>
TRM-codingd1cbf672025-06-18 15:15:08 +0800387 )}
388 </Content>
389 </div>
390 </div>
wu70fc8c52025-06-19 15:55:03 +0800391)
TRM-codingd1cbf672025-06-18 15:15:08 +0800392}