diff --git a/app_vx.py b/app_vx.py
index 51bc0fec..be1f176e 100644
--- a/app_vx.py
+++ b/app_vx.py
@@ -30,7 +30,6 @@ from werkzeug.security import generate_password_hash, check_password_hash
import json
from clickhouse_driver import Client as Cclient
import jwt
-import redis
from docx import Document
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
@@ -97,13 +96,14 @@ JWT_SECRET_KEY = 'vfllmgreat33818!' # 请修改为安全的密钥
JWT_ALGORITHM = 'HS256'
JWT_EXPIRATION_HOURS = 24 * 7 # Token有效期7天
-app.config['CACHE_TYPE'] = 'redis'
-app.config['CACHE_REDIS_HOST'] = '43.143.189.195' # 使用实际的服务器IP
-app.config['CACHE_REDIS_PORT'] = 6379
-app.config['CACHE_REDIS_PASSWORD'] = 'Zzl338180'
-app.config['CACHE_DEFAULT_TIMEOUT'] = 300
-app.config['SESSION_TYPE'] = 'redis'
-app.config['SESSION_REDIS'] = redis.Redis(host='43.143.189.195', port=6379, password='Zzl338180')
+# Session 配置 - 使用文件系统存储(替代 Redis)
+app.config['SESSION_TYPE'] = 'filesystem'
+app.config['SESSION_FILE_DIR'] = os.path.join(os.path.dirname(__file__), 'flask_session')
+app.config['SESSION_PERMANENT'] = True
+app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # Session 有效期 7 天
+
+# 确保 session 目录存在
+os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
# Cache directory setup
CACHE_DIR = Path('cache')
@@ -178,6 +178,216 @@ def beijing_now():
return datetime.now(beijing_tz)
+# ============================================
+# 订阅功能模块(与 app.py 保持一致)
+# ============================================
+class UserSubscription(db.Model):
+ """用户订阅表 - 独立于现有User表"""
+ __tablename__ = 'user_subscriptions'
+
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ user_id = db.Column(db.Integer, nullable=False, unique=True, index=True)
+ subscription_type = db.Column(db.String(10), nullable=False, default='free')
+ subscription_status = db.Column(db.String(20), nullable=False, default='active')
+ start_date = db.Column(db.DateTime, nullable=True)
+ end_date = db.Column(db.DateTime, nullable=True)
+ billing_cycle = db.Column(db.String(10), nullable=True)
+ auto_renewal = db.Column(db.Boolean, nullable=False, default=False)
+ created_at = db.Column(db.DateTime, default=beijing_now)
+ updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
+
+ def is_active(self):
+ if self.subscription_status != 'active':
+ return False
+ if self.subscription_type == 'free':
+ return True
+ if self.end_date:
+ # 将数据库的 naive datetime 转换为带时区的 aware datetime
+ beijing_tz = pytz.timezone('Asia/Shanghai')
+ end_date_aware = self.end_date if self.end_date.tzinfo else beijing_tz.localize(self.end_date)
+ return beijing_now() <= end_date_aware
+ return True
+
+ def days_left(self):
+ if not self.is_active():
+ return 0
+ if self.subscription_type == 'free':
+ return 999
+ if not self.end_date:
+ return 999
+ try:
+ now = beijing_now()
+ # 将数据库的 naive datetime 转换为带时区的 aware datetime
+ beijing_tz = pytz.timezone('Asia/Shanghai')
+ end_date_aware = self.end_date if self.end_date.tzinfo else beijing_tz.localize(self.end_date)
+ delta = end_date_aware - now
+ return max(0, delta.days)
+ except Exception as e:
+ return 0
+
+ def to_dict(self):
+ return {
+ 'type': self.subscription_type,
+ 'status': self.subscription_status,
+ 'is_active': self.is_active(),
+ 'start_date': self.start_date.isoformat() if self.start_date else None,
+ 'end_date': self.end_date.isoformat() if self.end_date else None,
+ 'days_left': self.days_left(),
+ 'billing_cycle': self.billing_cycle,
+ 'auto_renewal': self.auto_renewal
+ }
+
+
+# ============================================
+# 订阅等级工具函数
+# ============================================
+def get_user_subscription_safe(user_id):
+ """
+ 安全地获取用户订阅信息
+ :param user_id: 用户ID
+ :return: UserSubscription 对象或默认免费订阅
+ """
+ try:
+ subscription = UserSubscription.query.filter_by(user_id=user_id).first()
+ if not subscription:
+ # 如果用户没有订阅记录,创建默认免费订阅
+ subscription = UserSubscription(
+ user_id=user_id,
+ subscription_type='free',
+ subscription_status='active'
+ )
+ db.session.add(subscription)
+ db.session.commit()
+ return subscription
+ except Exception as e:
+ print(f"获取用户订阅信息失败: {e}")
+ # 返回一个临时的免费订阅对象(不保存到数据库)
+ temp_sub = UserSubscription(
+ user_id=user_id,
+ subscription_type='free',
+ subscription_status='active'
+ )
+ return temp_sub
+
+
+def _get_current_subscription_info():
+ """
+ 获取当前登录用户订阅信息的字典形式,未登录或异常时视为免费用户。
+ 小程序场景下从 request.current_user_id 获取用户ID
+ """
+ try:
+ user_id = getattr(request, 'current_user_id', None)
+ if not user_id:
+ return {
+ 'type': 'free',
+ 'status': 'active',
+ 'is_active': True
+ }
+ sub = get_user_subscription_safe(user_id)
+ return {
+ 'type': sub.subscription_type,
+ 'status': sub.subscription_status,
+ 'is_active': sub.is_active(),
+ 'start_date': sub.start_date.isoformat() if sub.start_date else None,
+ 'end_date': sub.end_date.isoformat() if sub.end_date else None,
+ 'days_left': sub.days_left()
+ }
+ except Exception as e:
+ print(f"获取订阅信息异常: {e}")
+ return {
+ 'type': 'free',
+ 'status': 'active',
+ 'is_active': True
+ }
+
+
+def _subscription_level(sub_type):
+ """将订阅类型映射到等级数值,free=0, pro=1, max=2。"""
+ mapping = {'free': 0, 'pro': 1, 'max': 2}
+ return mapping.get((sub_type or 'free').lower(), 0)
+
+
+def _has_required_level(required: str) -> bool:
+ """判断当前用户是否达到所需订阅级别。"""
+ info = _get_current_subscription_info()
+ if not info.get('is_active', True):
+ return False
+ return _subscription_level(info.get('type')) >= _subscription_level(required)
+
+
+# ============================================
+# 权限装饰器
+# ============================================
+def subscription_required(level='pro'):
+ """
+ 订阅等级装饰器 - 小程序专用
+ 用法:
+ @subscription_required('pro') # 需要 Pro 或 Max 用户
+ @subscription_required('max') # 仅限 Max 用户
+
+ 注意:此装饰器需要配合 @token_required 使用
+ """
+ from functools import wraps
+
+ def decorator(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not _has_required_level(level):
+ current_info = _get_current_subscription_info()
+ current_type = current_info.get('type', 'free')
+ is_active = current_info.get('is_active', False)
+
+ if not is_active:
+ return jsonify({
+ 'success': False,
+ 'error': '您的订阅已过期,请续费后继续使用',
+ 'error_code': 'SUBSCRIPTION_EXPIRED',
+ 'current_subscription': current_type,
+ 'required_subscription': level
+ }), 403
+
+ return jsonify({
+ 'success': False,
+ 'error': f'此功能需要 {level.upper()} 或更高等级会员',
+ 'error_code': 'SUBSCRIPTION_REQUIRED',
+ 'current_subscription': current_type,
+ 'required_subscription': level
+ }), 403
+
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+ return decorator
+
+
+def pro_or_max_required(f):
+ """
+ 快捷装饰器:要求 Pro 或 Max 用户(小程序专用场景)
+ 等同于 @subscription_required('pro')
+ """
+ from functools import wraps
+
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not _has_required_level('pro'):
+ current_info = _get_current_subscription_info()
+ current_type = current_info.get('type', 'free')
+
+ return jsonify({
+ 'success': False,
+ 'error': '小程序功能仅对 Pro 和 Max 会员开放',
+ 'error_code': 'MINIPROGRAM_PRO_REQUIRED',
+ 'current_subscription': current_type,
+ 'required_subscription': 'pro',
+ 'message': '请升级到 Pro 或 Max 会员以使用小程序完整功能'
+ }), 403
+
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+
class User(UserMixin, db.Model):
"""用户模型"""
id = db.Column(db.Integer, primary_key=True)
@@ -694,90 +904,72 @@ def verify_sms_code(phone_number, code):
return True, "验证成功"
-def send_notification_email(recipient, subject, template, **kwargs):
- """
- 发送通知邮件
- :param recipient: 收件人邮箱
- :param subject: 邮件主题
- :param template: 模板文件名
- :param kwargs: 传递给模板的参数
- """
- try:
- # 读取邮件模板内容
- msg = Message(
- subject=subject,
- sender=app.config['MAIL_USERNAME'],
- recipients=[recipient]
- )
-
- # 渲染HTML邮件内容
- if template == 'emails/notification_post_liked.html':
- msg.html = render_template_string("""
-
-
你的帖子收到了新的点赞
-
-
{{ liker.username }} 点赞了你的帖子
-
帖子内容: {{ post.content[:100] }}...
-
-
- 查看详情
-
-
- 如果你不想再收到此类通知,可以在个人设置中关闭邮件通知
-
-
- """, **kwargs)
-
- elif template == 'emails/notification_post_commented.html':
- msg.html = render_template_string("""
-
-
你的帖子收到了新的评论
-
-
{{ commenter.username }} 评论了你的帖子:
-
帖子内容: {{ post.content[:100] }}...
-
-
- 查看详情
-
-
- 如果你不想再收到此类通知,可以在个人设置中关闭邮件通知
-
-
- """, **kwargs)
-
- elif template == 'emails/notification_comment_replied.html':
- msg.html = render_template_string("""
-
-
你的评论收到了新的回复
-
-
{{ replier.username }} 回复了你的评论:
-
你的评论: {{ comment.content[:100] }}...
-
-
- 查看详情
-
-
- 如果你不想再收到此类通知,可以在个人设置中关闭邮件通知
-
-
- """, **kwargs)
-
- # 使用异步任务发送邮件
- send_async_email(msg)
- return True
-
- except Exception as e:
- app.logger.error(f"Error sending notification email: {str(e)}")
- return False
-
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+# ============================================
+# 订阅相关 API 接口(小程序专用)
+# ============================================
+@app.route('/api/subscription/info', methods=['GET'])
+@token_required
+def get_subscription_info():
+ """
+ 获取当前用户的订阅信息 - 小程序专用接口
+ 返回用户当前订阅类型、状态、剩余天数等信息
+ """
+ try:
+ info = _get_current_subscription_info()
+ return jsonify({
+ 'success': True,
+ 'data': info
+ })
+ except Exception as e:
+ print(f"获取订阅信息错误: {e}")
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'type': 'free',
+ 'status': 'active',
+ 'is_active': True,
+ 'days_left': 0
+ }
+ })
+
+
+@app.route('/api/subscription/check', methods=['GET'])
+@token_required
+def check_subscription_access():
+ """
+ 检查当前用户是否有权限使用小程序功能
+ 返回:是否为 Pro/Max 用户
+ """
+ try:
+ has_access = _has_required_level('pro')
+ info = _get_current_subscription_info()
+
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'has_access': has_access,
+ 'subscription_type': info.get('type', 'free'),
+ 'is_active': info.get('is_active', False),
+ 'message': '您可以使用小程序功能' if has_access else '小程序功能仅对 Pro 和 Max 会员开放'
+ }
+ })
+ except Exception as e:
+ print(f"检查订阅权限错误: {e}")
+ return jsonify({
+ 'success': False,
+ 'error': str(e)
+ }), 500
+
+
+# ============================================
+# 现有接口示例(应用权限控制)
+# ============================================
+
# 更新视图函数
@app.route('/settings/profile', methods=['POST'])
@token_required
@@ -860,8 +1052,10 @@ def get_clickhouse_client():
@app.route('/api/stock//kline')
+@token_required
+@pro_or_max_required
def get_stock_kline(stock_code):
- """获取股票K线数据"""
+ """获取股票K线数据 - 仅限 Pro/Max 会员(小程序功能)"""
chart_type = request.args.get('chart_type', 'daily') # 默认改为daily
event_time = request.args.get('event_time')
@@ -1341,102 +1535,6 @@ def like_post(post_id):
return jsonify({'success': False, 'message': '操作失败,请重试'})
-# 通知相关函数
-def notify_user_post_liked(post):
- """当用户的帖子被点赞时发送通知"""
- try:
- notification = Notification(
- user_id=post.user_id,
- type='post_like',
- content=f'{request.user.username} 点赞了你的帖子',
- link=url_for('event_detail', event_id=post.event_id, _anchor=f'post-{post.id}'),
- related_user_id=request.user.id,
- related_post_id=post.id
- )
- db.session.add(notification)
-
- # 如果用户开启了邮件通知
- user = User.query.get(post.user_id)
- if user.email_notifications:
- send_notification_email(
- recipient=user.email,
- subject='你的帖子收到了新的点赞',
- template='emails/notification_post_liked.html',
- user=user,
- post=post,
- liker=request.user
- )
-
- except Exception as e:
- app.logger.error(f"Error creating like notification: {str(e)}")
- # 通知创建失败不应影响主要功能
- pass
-
-
-def notify_user_post_commented(post):
- """当用户的帖子收到评论时发送通知"""
- try:
- notification = Notification(
- user_id=post.user_id,
- type='post_comment',
- content=f'{request.user.username} 评论了你的帖子',
- link=url_for('event_detail', event_id=post.event_id, _anchor=f'post-{post.id}'),
- related_user_id=request.user.id,
- related_post_id=post.id
- )
- db.session.add(notification)
-
- # 如果用户开启了邮件通知
- user = User.query.get(post.user_id)
- if user.email_notifications:
- send_notification_email(
- recipient=user.email,
- subject='你的帖子收到了新的评论',
- template='emails/notification_post_commented.html',
- user=user,
- post=post,
- commenter=request.user
- )
-
- except Exception as e:
- app.logger.error(f"Error creating comment notification: {str(e)}")
- pass
-
-
-def notify_user_comment_replied(parent_comment):
- """当用户的评论被回复时发送通知"""
- try:
- notification = Notification(
- user_id=parent_comment.user_id,
- type='comment_reply',
- content=f'{request.user.username} 回复了你的评论',
- link=url_for('event_detail',
- event_id=parent_comment.post.event_id,
- _anchor=f'comment-{parent_comment.id}'),
- related_user_id=request.user.id,
- related_post_id=parent_comment.post_id,
- related_comment_id=parent_comment.id
- )
- db.session.add(notification)
-
- # 如果用户开启了邮件通知
- user = User.query.get(parent_comment.user_id)
- if user.email_notifications:
- send_notification_email(
- recipient=user.email,
- subject='你的评论收到了新的回复',
- template='emails/notification_comment_replied.html',
- user=user,
- comment=parent_comment,
- replier=request.user
- )
-
- except Exception as e:
- app.logger.error(f"Error creating reply notification: {str(e)}")
- pass
-
-
-
def update_user_activity():
"""更新用户活跃度"""
with app.app_context():
@@ -1914,9 +2012,12 @@ def get_limit_rate(stock_code):
@app.route('/api/events', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_get_events():
"""
获取事件列表API - 优化版本(保持完全兼容)
+ 仅限 Pro/Max 会员访问(小程序功能)
优化策略:
1. 使用ind_type字段简化内部逻辑
@@ -2738,6 +2839,8 @@ def get_event_class(count):
else:
return 'bg-gradient-success'
@app.route('/api/calendar-event-counts')
+@token_required
+@pro_or_max_required
def get_calendar_event_counts():
"""获取整月的事件数量统计,仅统计type为event的事件"""
try:
@@ -2827,6 +2930,8 @@ def to_dict(self):
# 1. 首页接口
@app.route('/api/home', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_home():
try:
seven_days_ago = datetime.now() - timedelta(days=7)
@@ -3515,8 +3620,10 @@ def api_login_email():
# 5. 事件详情-相关标的接口
@app.route('/api/event//related-stocks-detail', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_event_related_stocks(event_id):
- """事件相关标的详情接口"""
+ """事件相关标的详情接口 - 仅限 Pro/Max 会员"""
try:
event = Event.query.get_or_404(event_id)
related_stocks = event.related_stocks.order_by(RelatedStock.correlation.desc()).all()
@@ -3702,9 +3809,11 @@ def api_event_related_stocks(event_id):
@app.route('/api/stock//minute-chart', methods=['GET'])
+@token_required
+@pro_or_max_required
def get_minute_chart_data(stock_code):
+ """获取股票分时图数据 - 仅限 Pro/Max 会员"""
client = get_clickhouse_client()
- """获取股票分时图数据"""
try:
# 获取当前日期或最新交易日的分时数据
from datetime import datetime, timedelta, time as dt_time
@@ -3776,8 +3885,10 @@ def get_minute_chart_data(stock_code):
@app.route('/api/event//stock//detail', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_stock_detail(event_id, stock_code):
- """个股详情接口"""
+ """个股详情接口 - 仅限 Pro/Max 会员"""
try:
# 验证事件是否存在
event = Event.query.get_or_404(event_id)
@@ -4035,6 +4146,8 @@ def get_stock_minute_chart_data(stock_code):
# 7. 事件详情-相关概念接口
@app.route('/api/event//related-concepts', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_event_related_concepts(event_id):
"""事件相关概念接口"""
try:
@@ -4076,6 +4189,8 @@ def api_event_related_concepts(event_id):
# 8. 事件详情-历史事件接口
@app.route('/api/event//historical-events', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_event_historical_events(event_id):
"""事件历史事件接口"""
try:
@@ -4176,6 +4291,7 @@ def api_event_historical_events(event_id):
@app.route('/api/event//comments', methods=['GET'])
@token_required
+@pro_or_max_required
def get_event_comments(event_id):
"""获取事件的所有评论和帖子(嵌套格式)
@@ -4430,6 +4546,7 @@ def get_event_comments(event_id):
@app.route('/api/comment//replies', methods=['GET'])
@token_required
+@pro_or_max_required
def get_comment_replies(comment_id):
"""获取某条评论的所有回复
@@ -4574,6 +4691,8 @@ def get_comment_replies(comment_id):
# 10. 投资日历-事件接口(增强版)
@app.route('/api/calendar/events', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_calendar_events():
"""投资日历事件接口 - 连接 future_events 表 (修正版)"""
try:
@@ -4829,6 +4948,8 @@ def api_calendar_events():
# 11. 投资日历-数据接口
@app.route('/api/calendar/data', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_calendar_data():
"""投资日历数据接口"""
try:
@@ -5015,8 +5136,10 @@ def extract_concepts_from_concepts_field(concepts_text):
@app.route('/api/calendar/detail/', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_future_event_detail(item_id):
- """未来事件详情接口 - 连接 future_events 表 (修正数据解析)"""
+ """未来事件详情接口 - 连接 future_events 表 (修正数据解析) - 仅限 Pro/Max 会员"""
try:
# 从 future_events 表查询事件详情
query = """
@@ -5249,6 +5372,8 @@ def api_future_event_detail(item_id):
# 13-15. 筛选弹窗接口(已有,优化格式)
@app.route('/api/filter/options', methods=['GET'])
+@token_required
+@pro_or_max_required
def api_filter_options():
"""筛选选项接口"""
try:
@@ -5795,89 +5920,6 @@ class UserFeedback(db.Model):
}
-# 21. 个人中心-意见反馈接口
-@app.route('/api/user/feedback', methods=['POST'])
-@token_required
-def api_user_feedback():
- """意见反馈接口"""
- try:
- data = request.get_json()
- feedback_type = data.get('type', 'other') # bug, suggestion, complaint, other
- content = data.get('content')
- contact_info = data.get('contact_info', '')
-
- if not content:
- return jsonify({
- 'code': 400,
- 'message': '反馈类型和内容不能为空',
- 'data': None
- }), 400
-
- # 验证反馈类型是否有效
- valid_types = ['bug', 'feature', 'suggestion', 'other'] # 可以根据需求修改
- if feedback_type not in valid_types:
- return jsonify({
- 'code': 400,
- 'message': '无效的反馈类型',
- 'data': None
- }), 400
-
- # 创建反馈记录
- feedback = UserFeedback(
- user_id=request.user.id,
- type=feedback_type,
- content=content,
- contact_info=contact_info
- )
-
- # 保存到数据库
- db.session.add(feedback)
- db.session.commit()
-
- # 可以在这里添加通知管理员的逻辑
- notify_admin_new_feedback(feedback)
-
- # 可以保存到数据库或发送邮件通知管理员
-
- return jsonify({
- 'code': 200,
- 'message': '反馈提交成功,我们会尽快处理',
- 'data': feedback.to_dict()
- })
- except Exception as e:
- return jsonify({
- 'code': 500,
- 'message': str(e),
- 'data': None
- }), 500
-
-
-def notify_admin_new_feedback(feedback):
- """通知管理员新的反馈"""
- try:
- # 获取管理员邮箱列表
- admin_emails = ['admin@example.com'] # 替换为实际的管理员邮箱列表
-
- # 发送通知邮件
- subject = f'新的用户反馈 - {feedback.type}'
- body = f"""
- 收到新的用户反馈:
-
- 用户ID: {feedback.user_id}
- 反馈类型: {feedback.type}
- 反馈内容: {feedback.content}
- 联系方式: {feedback.contact_info or '未提供'}
- 提交时间: {feedback.created_at}
- """
-
- for admin_email in admin_emails:
- send_notification_email(admin_email, subject, 'emails/admin_notification.html',
- feedback=feedback)
-
- except Exception as e:
- app.logger.error(f"发送管理员通知失败: {str(e)}")
-
-
# 通用错误处理
@app.errorhandler(404)
diff --git a/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js b/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
index 1dd23399..81b8b69e 100644
--- a/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
+++ b/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
@@ -162,7 +162,8 @@ const MiniKLineChart = React.memo(function MiniKLineChart({ stockCode, eventTime
{/* 单行紧凑布局:名称+涨跌幅 | 分时图 | K线图 | 关联描述 */}
-
- {/* 左侧:股票代码 + 名称 + 涨跌幅(垂直排列) - 收窄 */}
+
+ {/* 左侧:股票代码 + 名称 + 涨跌幅(垂直排列) */}
@@ -190,12 +190,10 @@ const StockListItem = ({
{/* 分时图 - 响应式宽度 */}
-
📈 分时
-
+
-
+
{/* K线图 - 响应式宽度 */}
-
📊 日线
-
+
-
+
{/* 关联描述 - 升级和降级处理 */}
{stock.relation_desc && (
diff --git a/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js b/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
index 2913ebb3..b6580af5 100644
--- a/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
+++ b/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
@@ -166,7 +166,8 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve