| 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 = 2; |
| 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(ADMIN_USER_ID) |
| setPosts(list) |
| } catch (e) { |
| if (e.message === 'Unauthorized') { |
| setHasPermission(false) |
| } else { |
| console.error(e) |
| } |
| } finally { |
| 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 }} />; |
| 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', |
| 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> |
| ); |
| } |