| // src/components/Admin.jsx |
| import React, { useState, useEffect, useCallback } from 'react' |
| import { useParams } from 'react-router-dom' |
| import 'antd/dist/antd.css' |
| import { |
| Layout, |
| Tabs, |
| Input, |
| List, |
| Card, |
| Button, |
| Tag, |
| Spin, |
| Typography, |
| Divider |
| } from 'antd' |
| import '../style/Admin.css' |
| import { fetchPosts, approvePost, rejectPost } from '../api/posts_trm' |
| |
| export default function AdminPage() { |
| const { userId } = useParams() // ← 从路由拿到 |
| const [posts, setPosts] = useState([]) |
| const [loading, setLoading] = useState(true) |
| const [hasPermission, setHasPermission] = 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() { |
| try { |
| const list = await fetchPosts(userId) // ← 传入 userId |
| setPosts(list) |
| } catch (e) { |
| if (e.message === 'Unauthorized') { |
| setHasPermission(false) |
| } else { |
| console.error(e) |
| } |
| } finally { |
| setLoading(false) |
| } |
| } |
| load() |
| }, [userId]) |
| |
| const sortedPosts = React.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]) |
| |
| const filteredPosts = React.useMemo(() => { |
| let list = sortedPosts |
| if (activeTab !== 'all') { |
| list = sortedPosts.filter(p => p.status === activeTab) |
| } |
| return list.filter(p => |
| p.title.toLowerCase().includes(searchTerm.toLowerCase()) |
| ) |
| }, [sortedPosts, activeTab, searchTerm]) |
| |
| const handleApprove = async id => { |
| await approvePost(id, userId) // ← 传入 userId |
| 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, userId) // ← 传入 userId |
| 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 }} |
| /> |
| ) |
| if (!hasPermission) |
| return ( |
| <div style={{ textAlign: 'center', marginTop: 100 }}> |
| 权限不足 |
| </div> |
| ) |
| |
| 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', |
| 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> |
| ) |
| } |