Files
vf_react/src/views/ValueForum/index.js
zdl f5dbdfa84c refactor(layout): 统一页面边距管理,移除 Container 限制
- layoutConfig.js: 新增 LAYOUT_PADDING 常量 { base: 4, md: 6, lg: '80px' }
- MainLayout.js: 在 Outlet 容器上统一应用 px={LAYOUT_PADDING.x}
- HomeNavbar.js: 边距从 lg:8 改为 lg:'80px',与内容区对齐
- AppFooter.js: 移除 Container,边距改为 lg:'80px'

页面组件清理(移除冗余的 px/Container):
- Company, Community, Center, Profile, Settings
- ValueForum, DataBrowser, LimitAnalyse, StockOverview, Concept

特殊处理:
- CompanyHeader: 使用负边距实现全宽背景
- Concept Hero: 使用负边距实现全宽背景

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 18:34:42 +08:00

498 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 价值论坛主页面
* 类似小红书/X的帖子广场
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Heading,
Text,
Button,
HStack,
VStack,
SimpleGrid,
Input,
InputGroup,
InputLeftElement,
Select,
Spinner,
Center,
useDisclosure,
Flex,
Badge,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Icon,
} from '@chakra-ui/react';
import { Search, PenSquare, TrendingUp, Clock, Heart, Zap, HelpCircle } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { forumColors } from '@theme/forumTheme';
import { getPosts, searchPosts } from '@services/elasticsearchService';
import { getTopics } from '@services/predictionMarketService.api';
import PostCard from './components/PostCard';
import PredictionTopicCard from './components/PredictionTopicCard';
import CreatePostModal from './components/CreatePostModal';
import CreatePredictionModal from './components/CreatePredictionModal';
import PredictionGuideModal from './components/PredictionGuideModal';
const MotionBox = motion(Box);
const ValueForum = () => {
const [posts, setPosts] = useState([]);
const [predictionTopics, setPredictionTopics] = useState([]);
const [loading, setLoading] = useState(true);
const [searchKeyword, setSearchKeyword] = useState('');
const [sortBy, setSortBy] = useState('created_at');
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [activeTab, setActiveTab] = useState(0);
const { isOpen: isPostModalOpen, onOpen: onPostModalOpen, onClose: onPostModalClose } = useDisclosure();
const { isOpen: isPredictionModalOpen, onOpen: onPredictionModalOpen, onClose: onPredictionModalClose } = useDisclosure();
const { isOpen: isGuideModalOpen, onOpen: onGuideModalOpen, onClose: onGuideModalClose } = useDisclosure();
// 获取帖子列表
const fetchPosts = async (currentPage = 1, reset = false) => {
try {
setLoading(true);
let result;
if (searchKeyword.trim()) {
result = await searchPosts(searchKeyword, {
page: currentPage,
size: 20,
});
} else {
result = await getPosts({
page: currentPage,
size: 20,
sort: sortBy,
order: 'desc',
});
}
if (reset) {
setPosts(result.posts);
} else {
setPosts((prev) => [...prev, ...result.posts]);
}
setTotal(result.total);
setHasMore(result.posts.length === 20);
} catch (error) {
console.error('获取帖子列表失败:', error);
} finally {
setLoading(false);
}
};
// 获取预测话题列表
const fetchPredictionTopics = async () => {
try {
setLoading(true);
const response = await getTopics({ status: 'active', sort_by: sortBy });
if (response.success) {
setPredictionTopics(response.data);
}
} catch (error) {
console.error('获取预测话题失败:', error);
} finally {
setLoading(false);
}
};
// 初始化加载
useEffect(() => {
if (activeTab === 0) {
fetchPosts(1, true);
} else {
fetchPredictionTopics();
}
}, [sortBy, activeTab]);
// 搜索处理
const handleSearch = () => {
setPage(1);
fetchPosts(1, true);
};
// 加载更多
const loadMore = () => {
const nextPage = page + 1;
setPage(nextPage);
fetchPosts(nextPage, false);
};
// 发帖成功回调
const handlePostCreated = () => {
setPage(1);
fetchPosts(1, true);
};
// 预测话题创建成功回调
const handlePredictionCreated = (newTopic) => {
setPredictionTopics((prev) => [newTopic, ...prev]);
};
// 排序选项
const sortOptions = [
{ value: 'created_at', label: '最新发布', icon: Clock },
{ value: 'likes_count', label: '最多点赞', icon: Heart },
{ value: 'views_count', label: '最多浏览', icon: TrendingUp },
];
return (
<Box
minH="100vh"
bg={forumColors.background.main}
pt="80px"
pb="20"
>
{/* padding 由 MainLayout 统一设置 */}
<Box>
{/* 顶部横幅 */}
<MotionBox
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
mb="10"
>
<VStack spacing="4" align="stretch">
{/* 标题区域 */}
<Flex justify="space-between" align="center">
<VStack align="start" spacing="2">
<Heading
as="h1"
fontSize="4xl"
fontWeight="bold"
bgGradient={forumColors.text.goldGradient}
bgClip="text"
>
价值论坛
</Heading>
<Text color={forumColors.text.secondary} fontSize="md">
分享投资见解追踪市场热点共同发现价值
</Text>
</VStack>
{/* 发帖按钮 */}
<HStack spacing="3">
<Button
leftIcon={<HelpCircle size={18} />}
variant="outline"
color={forumColors.primary[500]}
borderColor={forumColors.primary[500]}
size="lg"
fontWeight="bold"
onClick={onGuideModalOpen}
_hover={{
transform: 'translateY(-2px)',
bg: forumColors.primary[50],
borderColor: forumColors.primary[600],
}}
_active={{ transform: 'translateY(0)' }}
>
玩法说明
</Button>
<Button
leftIcon={<PenSquare size={18} />}
bg={forumColors.background.card}
color={forumColors.text.primary}
size="lg"
fontWeight="bold"
border="1px solid"
borderColor={forumColors.border.default}
onClick={onPostModalOpen}
_hover={{
transform: 'translateY(-2px)',
borderColor: forumColors.border.gold,
}}
_active={{ transform: 'translateY(0)' }}
>
发布帖子
</Button>
<Button
leftIcon={<Zap size={18} />}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
size="lg"
fontWeight="bold"
onClick={onPredictionModalOpen}
_hover={{
transform: 'translateY(-2px)',
boxShadow: forumColors.shadows.goldHover,
}}
_active={{ transform: 'translateY(0)' }}
>
发起预测
</Button>
</HStack>
</Flex>
{/* 搜索和筛选栏 */}
<Flex gap="4" align="center" flexWrap="wrap">
{/* 搜索框 */}
<InputGroup maxW="400px" flex="1">
<InputLeftElement pointerEvents="none">
<Search size={18} color={forumColors.text.tertiary} />
</InputLeftElement>
<Input
placeholder="搜索帖子标题、内容、标签..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
bg={forumColors.background.card}
border="1px solid"
borderColor={forumColors.border.default}
color={forumColors.text.primary}
_placeholder={{ color: forumColors.text.tertiary }}
_hover={{ borderColor: forumColors.border.light }}
_focus={{
borderColor: forumColors.border.gold,
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
}}
/>
</InputGroup>
{/* 排序选项 */}
<HStack spacing="2">
{sortOptions.map((option) => {
const Icon = option.icon;
const isActive = sortBy === option.value;
return (
<Button
key={option.value}
leftIcon={<Icon size={16} />}
size="md"
variant={isActive ? 'solid' : 'outline'}
bg={isActive ? forumColors.gradients.goldPrimary : 'transparent'}
color={isActive ? forumColors.background.main : forumColors.text.secondary}
borderColor={forumColors.border.default}
onClick={() => setSortBy(option.value)}
_hover={{
bg: isActive
? forumColors.gradients.goldPrimary
: forumColors.background.hover,
borderColor: forumColors.border.gold,
}}
>
{option.label}
</Button>
);
})}
</HStack>
</Flex>
{/* 统计信息 */}
<HStack spacing="6" color={forumColors.text.tertiary} fontSize="sm">
<Text>
<Text as="span" color={forumColors.primary[500]} fontWeight="bold">{total}</Text>
</Text>
</HStack>
</VStack>
</MotionBox>
{/* 标签页 */}
<Tabs
index={activeTab}
onChange={setActiveTab}
variant="soft-rounded"
colorScheme="yellow"
>
<TabList mb="8" bg={forumColors.background.card} p="2" borderRadius="xl">
<Tab
_selected={{
bg: forumColors.gradients.goldPrimary,
color: forumColors.background.main,
}}
>
<HStack spacing="2">
<Icon as={PenSquare} boxSize="16px" />
<Text>社区帖子</Text>
</HStack>
</Tab>
<Tab
_selected={{
bg: forumColors.gradients.goldPrimary,
color: forumColors.background.main,
}}
>
<HStack spacing="2">
<Icon as={Zap} boxSize="16px" />
<Text>预测市场</Text>
<Badge
bg="red.500"
color="white"
borderRadius="full"
px="2"
fontSize="xs"
>
NEW
</Badge>
</HStack>
</Tab>
</TabList>
<TabPanels>
{/* 普通帖子标签页 */}
<TabPanel p="0">
{loading && page === 1 ? (
<Center py="20">
<VStack spacing="4">
<Spinner
size="xl"
thickness="4px"
speed="0.8s"
color={forumColors.primary[500]}
/>
<Text color={forumColors.text.secondary}>加载中...</Text>
</VStack>
</Center>
) : posts.length === 0 ? (
<Center py="20">
<VStack spacing="4">
<Text color={forumColors.text.secondary} fontSize="lg">
{searchKeyword ? '未找到相关帖子' : '暂无帖子,快来发布第一篇吧!'}
</Text>
{!searchKeyword && (
<Button
leftIcon={<PenSquare size={18} />}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
onClick={onPostModalOpen}
_hover={{ opacity: 0.9 }}
>
发布帖子
</Button>
)}
</VStack>
</Center>
) : (
<>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="6">
<AnimatePresence>
{posts.map((post, index) => (
<MotionBox
key={post.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
>
<PostCard post={post} />
</MotionBox>
))}
</AnimatePresence>
</SimpleGrid>
{/* 加载更多按钮 */}
{hasMore && (
<Center mt="10">
<Button
onClick={loadMore}
isLoading={loading}
loadingText="加载中..."
bg={forumColors.background.card}
color={forumColors.text.primary}
border="1px solid"
borderColor={forumColors.border.default}
_hover={{
borderColor: forumColors.border.gold,
bg: forumColors.background.hover,
}}
>
加载更多
</Button>
</Center>
)}
</>
)}
</TabPanel>
{/* 预测市场标签页 */}
<TabPanel p="0">
{loading ? (
<Center py="20">
<VStack spacing="4">
<Spinner
size="xl"
thickness="4px"
speed="0.8s"
color={forumColors.primary[500]}
/>
<Text color={forumColors.text.secondary}>加载中...</Text>
</VStack>
</Center>
) : predictionTopics.length === 0 ? (
<Center py="20">
<VStack spacing="4">
<Icon as={Zap} boxSize="48px" color={forumColors.text.tertiary} />
<Text color={forumColors.text.secondary} fontSize="lg">
暂无预测话题快来发起第一个吧
</Text>
<Button
leftIcon={<Zap size={18} />}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
onClick={onPredictionModalOpen}
_hover={{ opacity: 0.9 }}
>
发起预测
</Button>
</VStack>
</Center>
) : (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing="6">
<AnimatePresence>
{predictionTopics.map((topic, index) => (
<MotionBox
key={topic.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
>
<PredictionTopicCard topic={topic} />
</MotionBox>
))}
</AnimatePresence>
</SimpleGrid>
)}
</TabPanel>
</TabPanels>
</Tabs>
</Box>
{/* 发帖模态框 */}
<CreatePostModal
isOpen={isPostModalOpen}
onClose={onPostModalClose}
onPostCreated={handlePostCreated}
/>
{/* 发起预测模态框 */}
<CreatePredictionModal
isOpen={isPredictionModalOpen}
onClose={onPredictionModalClose}
onTopicCreated={handlePredictionCreated}
/>
{/* 玩法说明模态框 */}
<PredictionGuideModal
isOpen={isGuideModalOpen}
onClose={onGuideModalClose}
/>
</Box>
);
};
export default ValueForum;