Initial commit
This commit is contained in:
695
src/views/Community/components/InvestmentCalendar.js
Normal file
695
src/views/Community/components/InvestmentCalendar.js
Normal file
@@ -0,0 +1,695 @@
|
||||
// src/views/Community/components/InvestmentCalendar.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card, Calendar, Badge, Modal, Table, Tabs, Tag, Button, List, Spin, Empty,
|
||||
Drawer, Typography, Divider, Space, Tooltip, message, Alert
|
||||
} from 'antd';
|
||||
import {
|
||||
StarFilled, StarOutlined, CalendarOutlined, LinkOutlined, StockOutlined,
|
||||
TagsOutlined, ClockCircleOutlined, InfoCircleOutlined, LockOutlined
|
||||
} from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { eventService, stockService } from '../../../services/eventService';
|
||||
import StockChartAntdModal from '../../../components/StockChart/StockChartAntdModal';
|
||||
import { useSubscription } from '../../../hooks/useSubscription';
|
||||
import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeModal';
|
||||
import './InvestmentCalendar.css';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Text, Title, Paragraph } = Typography;
|
||||
|
||||
const InvestmentCalendar = () => {
|
||||
// 权限控制
|
||||
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
||||
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
||||
|
||||
const [eventCounts, setEventCounts] = useState([]);
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
const [selectedDateEvents, setSelectedDateEvents] = useState([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentMonth, setCurrentMonth] = useState(moment());
|
||||
|
||||
// 新增状态
|
||||
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
|
||||
const [selectedDetail, setSelectedDetail] = useState(null);
|
||||
const [stockModalVisible, setStockModalVisible] = useState(false);
|
||||
const [selectedStocks, setSelectedStocks] = useState([]);
|
||||
const [stockQuotes, setStockQuotes] = useState({});
|
||||
const [klineModalVisible, setKlineModalVisible] = useState(false);
|
||||
const [selectedStock, setSelectedStock] = useState(null);
|
||||
const [followingIds, setFollowingIds] = useState([]); // 正在处理关注的事件ID列表
|
||||
const [addingToWatchlist, setAddingToWatchlist] = useState({}); // 正在添加到自选的股票代码
|
||||
const [expandedReasons, setExpandedReasons] = useState({}); // 跟踪每个股票关联理由的展开状态
|
||||
|
||||
// 加载月度事件统计
|
||||
const loadEventCounts = async (date) => {
|
||||
try {
|
||||
const year = date.year();
|
||||
const month = date.month() + 1;
|
||||
const response = await eventService.calendar.getEventCounts(year, month);
|
||||
|
||||
if (response.success) {
|
||||
setEventCounts(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load calendar event counts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载指定日期的事件
|
||||
const loadDateEvents = async (date) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const dateStr = date.format('YYYY-MM-DD');
|
||||
const response = await eventService.calendar.getEventsForDate(dateStr);
|
||||
|
||||
if (response.success) {
|
||||
setSelectedDateEvents(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load date events:', error);
|
||||
setSelectedDateEvents([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取六位股票代码(去掉后缀)
|
||||
const getSixDigitCode = (code) => {
|
||||
if (!code) return code;
|
||||
// 如果有.SH或.SZ后缀,去掉
|
||||
return code.split('.')[0];
|
||||
};
|
||||
|
||||
// 加载股票行情
|
||||
const loadStockQuotes = async (stocks, eventTime) => {
|
||||
try {
|
||||
const codes = stocks.map(stock => getSixDigitCode(stock[0])); // 确保使用六位代码
|
||||
const quotes = {};
|
||||
|
||||
// 使用市场API获取最新行情数据
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
const originalCode = stocks[i][0]; // 保持原始代码作为key
|
||||
try {
|
||||
const response = await fetch(`/api/market/trade/${code}?days=1`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
const latest = data.data[data.data.length - 1]; // 最新数据
|
||||
quotes[originalCode] = {
|
||||
price: latest.close,
|
||||
change: latest.change_amount,
|
||||
changePercent: latest.change_percent
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to load quote for ${code}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
setStockQuotes(quotes);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stock quotes:', error);
|
||||
message.error('加载股票行情失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadEventCounts(currentMonth);
|
||||
}, [currentMonth]);
|
||||
|
||||
// 自定义日期单元格渲染
|
||||
const dateCellRender = (value) => {
|
||||
const dateStr = value.format('YYYY-MM-DD');
|
||||
const dayEvents = eventCounts.find(item => item.date === dateStr);
|
||||
|
||||
if (dayEvents && dayEvents.count > 0) {
|
||||
return (
|
||||
<div className="calendar-events">
|
||||
{/* 使用小圆点指示器,不遮挡日期数字 */}
|
||||
<div className="event-indicators">
|
||||
<div
|
||||
className="event-dot"
|
||||
style={{ backgroundColor: getEventCountColor(dayEvents.count) }}
|
||||
title={`${dayEvents.count}个事件`}
|
||||
/>
|
||||
<span
|
||||
className={`event-count ${dayEvents.count >= 10 ? 'many-events' : ''}`}
|
||||
style={{ color: getEventCountColor(dayEvents.count) }}
|
||||
>
|
||||
{dayEvents.count > 99 ? '99+' : dayEvents.count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 根据事件数量获取颜色 - 更丰富的渐进色彩
|
||||
const getEventCountColor = (count) => {
|
||||
if (count >= 15) return '#f5222d'; // 深红色 - 非常多
|
||||
if (count >= 10) return '#fa541c'; // 橙红色 - 很多
|
||||
if (count >= 8) return '#fa8c16'; // 橙色 - 较多
|
||||
if (count >= 5) return '#faad14'; // 金黄色 - 中等
|
||||
if (count >= 3) return '#52c41a'; // 绿色 - 少量
|
||||
return '#1890ff'; // 蓝色 - 很少
|
||||
};
|
||||
|
||||
// 处理日期选择
|
||||
const handleDateSelect = (value) => {
|
||||
setSelectedDate(value);
|
||||
loadDateEvents(value);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 渲染重要性星级
|
||||
const renderStars = (star) => {
|
||||
const stars = [];
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
<StarFilled
|
||||
key={i}
|
||||
style={{
|
||||
color: i <= star ? '#faad14' : '#d9d9d9',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <span>{stars}</span>;
|
||||
};
|
||||
|
||||
// 显示内容详情
|
||||
const showContentDetail = (content, title) => {
|
||||
setSelectedDetail({ content, title });
|
||||
setDetailDrawerVisible(true);
|
||||
};
|
||||
|
||||
// 显示相关股票
|
||||
const showRelatedStocks = (stocks, eventTime) => {
|
||||
// 检查权限
|
||||
if (!hasFeatureAccess('related_stocks')) {
|
||||
setUpgradeModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stocks || stocks.length === 0) {
|
||||
message.info('暂无相关股票');
|
||||
return;
|
||||
}
|
||||
// 按相关度排序(限降序)
|
||||
const sortedStocks = [...stocks].sort((a, b) => (b[3] || 0) - (a[3] || 0));
|
||||
setSelectedStocks(sortedStocks);
|
||||
setStockModalVisible(true);
|
||||
loadStockQuotes(sortedStocks, eventTime);
|
||||
};
|
||||
|
||||
// 显示K线图
|
||||
const showKline = (stock) => {
|
||||
setSelectedStock({
|
||||
code: getSixDigitCode(stock[0]), // 确保使用六位代码
|
||||
name: stock[1]
|
||||
});
|
||||
setKlineModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理关注切换
|
||||
const handleFollowToggle = async (eventId) => {
|
||||
setFollowingIds(prev => [...prev, eventId]);
|
||||
try {
|
||||
const response = await eventService.calendar.toggleFollow(eventId);
|
||||
if (response.success) {
|
||||
// 更新本地事件列表的关注状态
|
||||
setSelectedDateEvents(prev =>
|
||||
prev.map(event =>
|
||||
event.id === eventId
|
||||
? { ...event, is_following: response.data.is_following }
|
||||
: event
|
||||
)
|
||||
);
|
||||
message.success(response.data.is_following ? '关注成功' : '取消关注成功');
|
||||
} else {
|
||||
message.error(response.error || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关注操作失败:', error);
|
||||
message.error('操作失败,请重试');
|
||||
} finally {
|
||||
setFollowingIds(prev => prev.filter(id => id !== eventId));
|
||||
}
|
||||
};
|
||||
|
||||
// 添加单只股票到自选
|
||||
const addSingleToWatchlist = async (stock) => {
|
||||
const stockCode = getSixDigitCode(stock[0]);
|
||||
|
||||
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true }));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/account/watchlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
stock_code: stockCode, // 使用六位代码
|
||||
stock_name: stock[1] // 股票名称
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
message.success(`已将 ${stock[1]}(${stockCode}) 添加到自选`);
|
||||
} else {
|
||||
message.error(data.error || '添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`添加${stock[1]}(${stockCode})到自选失败:`, error);
|
||||
message.error('添加失败,请重试');
|
||||
} finally {
|
||||
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 事件表格列定义
|
||||
const eventColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'calendar_time',
|
||||
key: 'time',
|
||||
width: 80,
|
||||
render: (time) => (
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
<Text>{moment(time).format('HH:mm')}</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '重要度',
|
||||
dataIndex: 'star',
|
||||
key: 'star',
|
||||
width: 120,
|
||||
render: renderStars
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true,
|
||||
render: (text) => (
|
||||
<Tooltip title={text}>
|
||||
<Text strong>{text}</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '背景',
|
||||
dataIndex: 'former',
|
||||
key: 'former',
|
||||
width: 80,
|
||||
render: (text) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => showContentDetail(text + (text ? '\n\n(AI合成)' : ''), '事件背景')}
|
||||
disabled={!text}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: (
|
||||
<span>
|
||||
相关股票
|
||||
{!hasFeatureAccess('related_stocks') && (
|
||||
<LockOutlined style={{ marginLeft: '4px', fontSize: '12px', opacity: 0.6, color: '#faad14' }} />
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'related_stocks',
|
||||
key: 'stocks',
|
||||
width: 100,
|
||||
render: (stocks, record) => {
|
||||
const hasStocks = stocks && stocks.length > 0;
|
||||
const hasAccess = hasFeatureAccess('related_stocks');
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={hasAccess ? <StockOutlined /> : <LockOutlined />}
|
||||
onClick={() => showRelatedStocks(stocks, record.calendar_time)}
|
||||
disabled={!hasStocks}
|
||||
style={!hasAccess ? { color: '#faad14' } : {}}
|
||||
>
|
||||
{hasStocks ? (hasAccess ? `${stocks.length}只` : '🔒需Pro') : '无'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '相关概念',
|
||||
dataIndex: 'concepts',
|
||||
key: 'concepts',
|
||||
width: 200,
|
||||
render: (concepts) => (
|
||||
<Space wrap>
|
||||
{concepts && concepts.length > 0 ? (
|
||||
concepts.slice(0, 3).map((concept, index) => (
|
||||
<Tag key={index} icon={<TagsOutlined />}>
|
||||
{Array.isArray(concept) ? concept[0] : concept}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">无</Text>
|
||||
)}
|
||||
{concepts && concepts.length > 3 && (
|
||||
<Tag>+{concepts.length - 3}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '关注',
|
||||
key: 'follow',
|
||||
width: 60,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type={record.is_following ? "primary" : "default"}
|
||||
icon={record.is_following ? <StarFilled /> : <StarOutlined />}
|
||||
size="small"
|
||||
onClick={() => handleFollowToggle(record.id)}
|
||||
loading={followingIds.includes(record.id)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 股票表格列定义
|
||||
const stockColumns = [
|
||||
{
|
||||
title: '代码',
|
||||
dataIndex: '0',
|
||||
key: 'code',
|
||||
width: 100,
|
||||
render: (code) => {
|
||||
const sixDigitCode = getSixDigitCode(code);
|
||||
return (
|
||||
<a
|
||||
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Text code>{sixDigitCode}</Text>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: '1',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
render: (name, record) => {
|
||||
const sixDigitCode = getSixDigitCode(record[0]);
|
||||
return (
|
||||
<a
|
||||
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Text strong>{name}</Text>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '现价',
|
||||
key: 'price',
|
||||
width: 80,
|
||||
render: (_, record) => {
|
||||
const quote = stockQuotes[record[0]];
|
||||
if (quote && quote.price !== undefined) {
|
||||
return (
|
||||
<Text type={quote.change > 0 ? 'danger' : 'success'}>
|
||||
{quote.price?.toFixed(2)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return <Text>-</Text>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '涨跌幅',
|
||||
key: 'change',
|
||||
width: 100,
|
||||
render: (_, record) => {
|
||||
const quote = stockQuotes[record[0]];
|
||||
if (quote && quote.changePercent !== undefined) {
|
||||
const changePercent = quote.changePercent || 0;
|
||||
return (
|
||||
<Tag color={changePercent > 0 ? 'red' : 'green'}>
|
||||
{changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}%
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return <Text>-</Text>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '关联理由',
|
||||
dataIndex: '2',
|
||||
key: 'reason',
|
||||
render: (reason, record) => {
|
||||
const stockCode = record[0];
|
||||
const isExpanded = expandedReasons[stockCode] || false;
|
||||
const shouldTruncate = reason && reason.length > 100;
|
||||
|
||||
const toggleExpanded = () => {
|
||||
setExpandedReasons(prev => ({
|
||||
...prev,
|
||||
[stockCode]: !prev[stockCode]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text>
|
||||
{isExpanded || !shouldTruncate
|
||||
? reason
|
||||
: `${reason?.slice(0, 100)}...`
|
||||
}
|
||||
</Text>
|
||||
{shouldTruncate && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={toggleExpanded}
|
||||
style={{ padding: 0, marginLeft: 4 }}
|
||||
>
|
||||
({isExpanded ? '收起' : '展开'})
|
||||
</Button>
|
||||
)}
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>(AI合成)</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: 'K线图',
|
||||
key: 'kline',
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => showKline(record)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_, record) => {
|
||||
const stockCode = getSixDigitCode(record[0]);
|
||||
const isAdding = addingToWatchlist[stockCode] || false;
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
loading={isAdding}
|
||||
onClick={() => addSingleToWatchlist(record)}
|
||||
>
|
||||
加自选
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title={
|
||||
<span>
|
||||
<CalendarOutlined style={{ marginRight: 8 }} />
|
||||
投资日历
|
||||
</span>
|
||||
}
|
||||
className="investment-calendar"
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Calendar
|
||||
fullscreen={false}
|
||||
dateCellRender={dateCellRender}
|
||||
onSelect={handleDateSelect}
|
||||
onPanelChange={(date) => setCurrentMonth(date)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 事件列表模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<CalendarOutlined />
|
||||
<span>{selectedDate?.format('YYYY年MM月DD日')} 投资事件</span>
|
||||
</Space>
|
||||
}
|
||||
visible={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={1200}
|
||||
footer={null}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Tabs defaultActiveKey="event">
|
||||
<TabPane tab={`事件 (${selectedDateEvents.filter(e => e.type === 'event').length})`} key="event">
|
||||
<Table
|
||||
dataSource={selectedDateEvents.filter(e => e.type === 'event')}
|
||||
columns={eventColumns}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
pagination={false}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab={`数据 (${selectedDateEvents.filter(e => e.type === 'data').length})`} key="data">
|
||||
<Table
|
||||
dataSource={selectedDateEvents.filter(e => e.type === 'data')}
|
||||
columns={eventColumns}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
pagination={false}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
</Modal>
|
||||
|
||||
{/* 内容详情抽屉 */}
|
||||
<Drawer
|
||||
title={selectedDetail?.title}
|
||||
placement="right"
|
||||
width={600}
|
||||
onClose={() => setDetailDrawerVisible(false)}
|
||||
visible={detailDrawerVisible}
|
||||
>
|
||||
<div className="markdown-content">
|
||||
<ReactMarkdown>{selectedDetail?.content || '暂无内容'}</ReactMarkdown>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
{/* 相关股票模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<StockOutlined />
|
||||
<span>相关股票</span>
|
||||
{!hasFeatureAccess('related_stocks') && (
|
||||
<LockOutlined style={{ color: '#faad14' }} />
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
visible={stockModalVisible}
|
||||
onCancel={() => {
|
||||
setStockModalVisible(false);
|
||||
setExpandedReasons({}); // 清理展开状态
|
||||
setAddingToWatchlist({}); // 清理加自选状态
|
||||
}}
|
||||
width={1000}
|
||||
footer={
|
||||
<Button onClick={() => setStockModalVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{hasFeatureAccess('related_stocks') ? (
|
||||
<Table
|
||||
dataSource={selectedStocks}
|
||||
columns={stockColumns}
|
||||
rowKey={(record) => record[0]}
|
||||
size="middle"
|
||||
pagination={false}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px', opacity: 0.3 }}>
|
||||
<LockOutlined />
|
||||
</div>
|
||||
<Alert
|
||||
message="相关股票功能已锁定"
|
||||
description="此功能需要Pro版订阅才能使用"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ maxWidth: '400px', margin: '0 auto', marginBottom: '24px' }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => setUpgradeModalOpen(true)}
|
||||
>
|
||||
升级到 Pro版
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* K线图模态框 */}
|
||||
{klineModalVisible && selectedStock && (
|
||||
<StockChartAntdModal
|
||||
open={klineModalVisible}
|
||||
stock={selectedStock}
|
||||
onCancel={() => setKlineModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 订阅升级模态框 */}
|
||||
<SubscriptionUpgradeModal
|
||||
isOpen={upgradeModalOpen}
|
||||
onClose={() => setUpgradeModalOpen(false)}
|
||||
requiredLevel="pro"
|
||||
featureName="相关股票分析"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvestmentCalendar;
|
||||
Reference in New Issue
Block a user