事件标记线bug修复
This commit is contained in:
105
app.py
105
app.py
@@ -5641,6 +5641,23 @@ class FutureEventFollow(db.Model):
|
|||||||
__table_args__ = (db.UniqueConstraint('user_id', 'future_event_id'),)
|
__table_args__ = (db.UniqueConstraint('user_id', 'future_event_id'),)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSentimentVote(db.Model):
|
||||||
|
"""事件情绪投票(看多/看空)"""
|
||||||
|
__tablename__ = 'event_sentiment_vote'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||||||
|
vote_type = db.Column(db.String(10), nullable=False) # 'bullish' 或 'bearish'
|
||||||
|
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||||
|
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||||||
|
|
||||||
|
user = db.relationship('User', backref='sentiment_votes')
|
||||||
|
event = db.relationship('Event', backref='sentiment_votes')
|
||||||
|
|
||||||
|
__table_args__ = (db.UniqueConstraint('user_id', 'event_id'),)
|
||||||
|
|
||||||
|
|
||||||
# —— 自选股输入统一化与名称补全工具 ——
|
# —— 自选股输入统一化与名称补全工具 ——
|
||||||
def _normalize_stock_input(raw_input: str):
|
def _normalize_stock_input(raw_input: str):
|
||||||
"""解析用户输入为标准6位股票代码与可选名称。
|
"""解析用户输入为标准6位股票代码与可选名称。
|
||||||
@@ -6687,6 +6704,10 @@ class Event(db.Model):
|
|||||||
post_count = db.Column(db.Integer, default=0)
|
post_count = db.Column(db.Integer, default=0)
|
||||||
follower_count = db.Column(db.Integer, default=0)
|
follower_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# 看多/看空投票统计
|
||||||
|
bullish_count = db.Column(db.Integer, default=0) # 看多数
|
||||||
|
bearish_count = db.Column(db.Integer, default=0) # 看空数
|
||||||
|
|
||||||
# 关联信息
|
# 关联信息
|
||||||
related_industries = db.Column(db.String(20)) # 申万行业代码,如 "S640701"
|
related_industries = db.Column(db.String(20)) # 申万行业代码,如 "S640701"
|
||||||
keywords = db.Column(db.JSON)
|
keywords = db.Column(db.JSON)
|
||||||
@@ -7565,6 +7586,74 @@ def toggle_event_follow(event_id):
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/events/<int:event_id>/sentiment-vote', methods=['POST'])
|
||||||
|
def vote_event_sentiment(event_id):
|
||||||
|
"""事件情绪投票(看多/看空)"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = Event.query.get_or_404(event_id)
|
||||||
|
user_id = session['user_id']
|
||||||
|
data = request.get_json() or {}
|
||||||
|
vote_type = data.get('vote_type') # 'bullish', 'bearish', 或 null/None(取消投票)
|
||||||
|
|
||||||
|
# 验证投票类型
|
||||||
|
if vote_type and vote_type not in ('bullish', 'bearish'):
|
||||||
|
return jsonify({'success': False, 'error': '无效的投票类型'}), 400
|
||||||
|
|
||||||
|
existing = EventSentimentVote.query.filter_by(user_id=user_id, event_id=event_id).first()
|
||||||
|
old_vote_type = existing.vote_type if existing else None
|
||||||
|
|
||||||
|
if vote_type is None or vote_type == '':
|
||||||
|
# 取消投票
|
||||||
|
if existing:
|
||||||
|
if old_vote_type == 'bullish':
|
||||||
|
event.bullish_count = max(0, (event.bullish_count or 0) - 1)
|
||||||
|
elif old_vote_type == 'bearish':
|
||||||
|
event.bearish_count = max(0, (event.bearish_count or 0) - 1)
|
||||||
|
db.session.delete(existing)
|
||||||
|
elif existing:
|
||||||
|
# 更新投票
|
||||||
|
if old_vote_type != vote_type:
|
||||||
|
# 减少旧投票计数
|
||||||
|
if old_vote_type == 'bullish':
|
||||||
|
event.bullish_count = max(0, (event.bullish_count or 0) - 1)
|
||||||
|
elif old_vote_type == 'bearish':
|
||||||
|
event.bearish_count = max(0, (event.bearish_count or 0) - 1)
|
||||||
|
# 增加新投票计数
|
||||||
|
if vote_type == 'bullish':
|
||||||
|
event.bullish_count = (event.bullish_count or 0) + 1
|
||||||
|
elif vote_type == 'bearish':
|
||||||
|
event.bearish_count = (event.bearish_count or 0) + 1
|
||||||
|
existing.vote_type = vote_type
|
||||||
|
else:
|
||||||
|
# 新建投票
|
||||||
|
vote = EventSentimentVote(user_id=user_id, event_id=event_id, vote_type=vote_type)
|
||||||
|
db.session.add(vote)
|
||||||
|
if vote_type == 'bullish':
|
||||||
|
event.bullish_count = (event.bullish_count or 0) + 1
|
||||||
|
elif vote_type == 'bearish':
|
||||||
|
event.bearish_count = (event.bearish_count or 0) + 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 查询当前用户的投票状态
|
||||||
|
current_vote = EventSentimentVote.query.filter_by(user_id=user_id, event_id=event_id).first()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'user_vote': current_vote.vote_type if current_vote else None,
|
||||||
|
'bullish_count': event.bullish_count or 0,
|
||||||
|
'bearish_count': event.bearish_count or 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/events/<int:event_id>/transmission', methods=['GET'])
|
@app.route('/api/events/<int:event_id>/transmission', methods=['GET'])
|
||||||
def get_transmission_chain(event_id):
|
def get_transmission_chain(event_id):
|
||||||
try:
|
try:
|
||||||
@@ -10968,6 +11057,19 @@ def api_get_events():
|
|||||||
query = query.order_by(order_func(Event.view_count))
|
query = query.order_by(order_func(Event.view_count))
|
||||||
# 分页
|
# 分页
|
||||||
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
|
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
# 查询当前用户的投票状态
|
||||||
|
user_vote_map = {}
|
||||||
|
if 'user_id' in session:
|
||||||
|
user_id = session['user_id']
|
||||||
|
event_ids = [event.id for event in paginated.items]
|
||||||
|
if event_ids:
|
||||||
|
user_votes = EventSentimentVote.query.filter(
|
||||||
|
EventSentimentVote.user_id == user_id,
|
||||||
|
EventSentimentVote.event_id.in_(event_ids)
|
||||||
|
).all()
|
||||||
|
user_vote_map = {v.event_id: v.vote_type for v in user_votes}
|
||||||
|
|
||||||
events_data = []
|
events_data = []
|
||||||
for event in paginated.items:
|
for event in paginated.items:
|
||||||
event_dict = {
|
event_dict = {
|
||||||
@@ -10988,6 +11090,9 @@ def api_get_events():
|
|||||||
'view_count': event.view_count,
|
'view_count': event.view_count,
|
||||||
'post_count': event.post_count,
|
'post_count': event.post_count,
|
||||||
'follower_count': event.follower_count,
|
'follower_count': event.follower_count,
|
||||||
|
'bullish_count': event.bullish_count or 0,
|
||||||
|
'bearish_count': event.bearish_count or 0,
|
||||||
|
'user_vote': user_vote_map.get(event.id), # 用户投票状态
|
||||||
'related_avg_chg': event.related_avg_chg,
|
'related_avg_chg': event.related_avg_chg,
|
||||||
'related_max_chg': event.related_max_chg,
|
'related_max_chg': event.related_max_chg,
|
||||||
'related_week_chg': event.related_week_chg,
|
'related_week_chg': event.related_week_chg,
|
||||||
|
|||||||
@@ -7392,6 +7392,7 @@ if __name__ == '__main__':
|
|||||||
logger.info(f"🚀 启动 Flask 应用: port={args.port}, debug={args.debug}, ssl={'enabled' if ssl_context else 'disabled'}")
|
logger.info(f"🚀 启动 Flask 应用: port={args.port}, debug={args.debug}, ssl={'enabled' if ssl_context else 'disabled'}")
|
||||||
|
|
||||||
app.run(
|
app.run(
|
||||||
|
|
||||||
host='0.0.0.0',
|
host='0.0.0.0',
|
||||||
port=args.port,
|
port=args.port,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
|
|||||||
@@ -888,6 +888,10 @@ export function generateMockEvents(params = {}) {
|
|||||||
const tradingDate = new Date(createdAt);
|
const tradingDate = new Date(createdAt);
|
||||||
tradingDate.setDate(tradingDate.getDate() + 1);
|
tradingDate.setDate(tradingDate.getDate() + 1);
|
||||||
|
|
||||||
|
// 生成投票数据
|
||||||
|
const bullishCount = Math.floor(Math.random() * 100) + 5;
|
||||||
|
const bearishCount = Math.floor(Math.random() * 50) + 3;
|
||||||
|
|
||||||
allEvents.push({
|
allEvents.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
title: generateEventTitle(industry, i),
|
title: generateEventTitle(industry, i),
|
||||||
@@ -901,6 +905,10 @@ export function generateMockEvents(params = {}) {
|
|||||||
trading_date: tradingDate.toISOString().split('T')[0], // YYYY-MM-DD 格式
|
trading_date: tradingDate.toISOString().split('T')[0], // YYYY-MM-DD 格式
|
||||||
hot_score: hotScore,
|
hot_score: hotScore,
|
||||||
view_count: Math.floor(Math.random() * 10000),
|
view_count: Math.floor(Math.random() * 10000),
|
||||||
|
follower_count: Math.floor(Math.random() * 500) + 10,
|
||||||
|
bullish_count: bullishCount,
|
||||||
|
bearish_count: bearishCount,
|
||||||
|
user_vote: null, // 默认未投票
|
||||||
related_avg_chg: parseFloat(relatedAvgChg),
|
related_avg_chg: parseFloat(relatedAvgChg),
|
||||||
related_max_chg: parseFloat(relatedMaxChg),
|
related_max_chg: parseFloat(relatedMaxChg),
|
||||||
related_week_chg: parseFloat(relatedWeekChg),
|
related_week_chg: parseFloat(relatedWeekChg),
|
||||||
@@ -1374,6 +1382,10 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
|
|||||||
...generateKeywords(industry, i).slice(0, 2)
|
...generateKeywords(industry, i).slice(0, 2)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 生成投票数据
|
||||||
|
const bullishCount = Math.floor(Math.random() * 80) + 10;
|
||||||
|
const bearishCount = Math.floor(Math.random() * 40) + 5;
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
id: `dynamic_${i + 1}`,
|
id: `dynamic_${i + 1}`,
|
||||||
title: eventTitle,
|
title: eventTitle,
|
||||||
@@ -1387,6 +1399,9 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
|
|||||||
hot_score: hotScore,
|
hot_score: hotScore,
|
||||||
view_count: Math.floor(Math.random() * 5000) + 1000, // 1000-6000 浏览量
|
view_count: Math.floor(Math.random() * 5000) + 1000, // 1000-6000 浏览量
|
||||||
follower_count: Math.floor(Math.random() * 500) + 50, // 50-550 关注数
|
follower_count: Math.floor(Math.random() * 500) + 50, // 50-550 关注数
|
||||||
|
bullish_count: bullishCount, // 看多数
|
||||||
|
bearish_count: bearishCount, // 看空数
|
||||||
|
user_vote: null, // 默认未投票
|
||||||
post_count: Math.floor(Math.random() * 100) + 10, // 10-110 帖子数
|
post_count: Math.floor(Math.random() * 100) + 10, // 10-110 帖子数
|
||||||
related_avg_chg: parseFloat(relatedAvgChg),
|
related_avg_chg: parseFloat(relatedAvgChg),
|
||||||
related_max_chg: parseFloat(relatedMaxChg),
|
related_max_chg: parseFloat(relatedMaxChg),
|
||||||
|
|||||||
@@ -601,6 +601,56 @@ export const eventHandlers = [
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 事件情绪投票(看多/看空)
|
||||||
|
http.post('/api/events/:eventId/sentiment-vote', async ({ params, request }) => {
|
||||||
|
await delay(200);
|
||||||
|
|
||||||
|
const { eventId } = params;
|
||||||
|
const numericEventId = parseInt(eventId, 10);
|
||||||
|
|
||||||
|
console.log('[Mock] 事件情绪投票, eventId:', numericEventId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const voteType = body.vote_type; // 'bullish', 'bearish', 或 null
|
||||||
|
|
||||||
|
// 使用内存状态管理投票
|
||||||
|
// 简单模拟:根据 eventId 生成基础数据
|
||||||
|
const baseBullish = (numericEventId * 7) % 50 + 10;
|
||||||
|
const baseBearish = (numericEventId * 3) % 30 + 5;
|
||||||
|
|
||||||
|
// 根据投票类型调整计数
|
||||||
|
let bullishCount = baseBullish;
|
||||||
|
let bearishCount = baseBearish;
|
||||||
|
|
||||||
|
if (voteType === 'bullish') {
|
||||||
|
bullishCount += 1;
|
||||||
|
} else if (voteType === 'bearish') {
|
||||||
|
bearishCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user_vote: voteType || null,
|
||||||
|
bullish_count: bullishCount,
|
||||||
|
bearish_count: bearishCount,
|
||||||
|
},
|
||||||
|
message: voteType ? '投票成功' : '取消投票成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock] 事件情绪投票失败:', error);
|
||||||
|
return HttpResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: '投票失败',
|
||||||
|
data: null
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// 获取事件传导链分析数据
|
// 获取事件传导链分析数据
|
||||||
http.get('/api/events/:eventId/transmission', async ({ params }) => {
|
http.get('/api/events/:eventId/transmission', async ({ params }) => {
|
||||||
await delay(500);
|
await delay(500);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
ImportanceStamp,
|
ImportanceStamp,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventFollowButton,
|
EventFollowButton,
|
||||||
|
EventEngagement,
|
||||||
KeywordsCarousel,
|
KeywordsCarousel,
|
||||||
} from './atoms';
|
} from './atoms';
|
||||||
import StockChangeIndicators from '@components/StockChangeIndicators';
|
import StockChangeIndicators from '@components/StockChangeIndicators';
|
||||||
@@ -38,10 +39,12 @@ import StockChangeIndicators from '@components/StockChangeIndicators';
|
|||||||
* @param {Function} props.onEventClick - 卡片点击事件
|
* @param {Function} props.onEventClick - 卡片点击事件
|
||||||
* @param {Function} props.onTitleClick - 标题点击事件
|
* @param {Function} props.onTitleClick - 标题点击事件
|
||||||
* @param {Function} props.onToggleFollow - 切换关注事件
|
* @param {Function} props.onToggleFollow - 切换关注事件
|
||||||
|
* @param {Function} props.onVoteChange - 投票变化回调
|
||||||
* @param {Object} props.timelineStyle - 时间轴样式配置
|
* @param {Object} props.timelineStyle - 时间轴样式配置
|
||||||
* @param {string} props.borderColor - 边框颜色
|
* @param {string} props.borderColor - 边框颜色
|
||||||
* @param {string} props.indicatorSize - 涨幅指标尺寸 ('default' | 'comfortable' | 'large')
|
* @param {string} props.indicatorSize - 涨幅指标尺寸 ('default' | 'comfortable' | 'large')
|
||||||
* @param {string} props.layout - 布局模式 ('vertical' | 'four-row'),影响时间轴竖线高度
|
* @param {string} props.layout - 布局模式 ('vertical' | 'four-row'),影响时间轴竖线高度
|
||||||
|
* @param {boolean} props.showEngagement - 是否显示互动指标(浏览/关注/投票)
|
||||||
*/
|
*/
|
||||||
const HorizontalDynamicNewsEventCard = React.memo(({
|
const HorizontalDynamicNewsEventCard = React.memo(({
|
||||||
event,
|
event,
|
||||||
@@ -52,10 +55,12 @@ const HorizontalDynamicNewsEventCard = React.memo(({
|
|||||||
onEventClick,
|
onEventClick,
|
||||||
onTitleClick,
|
onTitleClick,
|
||||||
onToggleFollow,
|
onToggleFollow,
|
||||||
|
onVoteChange,
|
||||||
timelineStyle,
|
timelineStyle,
|
||||||
borderColor: timelineBorderColor,
|
borderColor: timelineBorderColor,
|
||||||
indicatorSize = 'comfortable',
|
indicatorSize = 'comfortable',
|
||||||
layout = 'vertical',
|
layout = 'vertical',
|
||||||
|
showEngagement = true,
|
||||||
}) => {
|
}) => {
|
||||||
const importance = getImportanceConfig(event.importance);
|
const importance = getImportanceConfig(event.importance);
|
||||||
const { isMobile } = useDevice();
|
const { isMobile } = useDevice();
|
||||||
@@ -247,13 +252,29 @@ const HorizontalDynamicNewsEventCard = React.memo(({
|
|||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* 第二行:涨跌幅数据 */}
|
{/* 第二行:涨跌幅数据 + 互动指标 */}
|
||||||
|
<HStack justify="space-between" align="center" flexWrap="wrap" gap={1}>
|
||||||
<StockChangeIndicators
|
<StockChangeIndicators
|
||||||
maxChange={event.related_max_chg}
|
maxChange={event.related_max_chg}
|
||||||
avgChange={event.related_avg_chg}
|
avgChange={event.related_avg_chg}
|
||||||
expectationScore={event.expectation_surprise_score}
|
expectationScore={event.expectation_surprise_score}
|
||||||
size={indicatorSize}
|
size={indicatorSize}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 互动指标:浏览量、关注数、看多/看空 */}
|
||||||
|
{showEngagement && (
|
||||||
|
<EventEngagement
|
||||||
|
eventId={event.id}
|
||||||
|
viewCount={event.view_count}
|
||||||
|
followerCount={event.follower_count}
|
||||||
|
bullishCount={event.bullish_count}
|
||||||
|
bearishCount={event.bearish_count}
|
||||||
|
userVote={event.user_vote}
|
||||||
|
size="xs"
|
||||||
|
onVoteChange={onVoteChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
// src/views/Community/components/EventCard/atoms/EventEngagement.js
|
||||||
|
// 事件互动组件 - 显示浏览量、关注量和看多/看空投票
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
Badge,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ViewIcon, StarIcon } from '@chakra-ui/icons';
|
||||||
|
import { TbArrowBigUp, TbArrowBigDown, TbArrowBigUpFilled, TbArrowBigDownFilled } from 'react-icons/tb';
|
||||||
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
import { useAuth } from '@contexts/AuthContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字(大于1000显示为 1k, 1.2k 等)
|
||||||
|
* @param {number} num - 原始数字
|
||||||
|
* @returns {string} 格式化后的字符串
|
||||||
|
*/
|
||||||
|
const formatCount = (num) => {
|
||||||
|
if (num == null || isNaN(num)) return '0';
|
||||||
|
if (num >= 10000) return `${(num / 10000).toFixed(1)}w`;
|
||||||
|
if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
|
||||||
|
return String(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件互动组件
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {number} props.eventId - 事件ID
|
||||||
|
* @param {number} props.viewCount - 浏览量
|
||||||
|
* @param {number} props.followerCount - 关注数
|
||||||
|
* @param {number} props.bullishCount - 看多数
|
||||||
|
* @param {number} props.bearishCount - 看空数
|
||||||
|
* @param {string} props.userVote - 用户当前投票状态 ('bullish' | 'bearish' | null)
|
||||||
|
* @param {string} props.size - 尺寸 ('xs' | 'sm' | 'md')
|
||||||
|
* @param {boolean} props.showVoting - 是否显示投票功能
|
||||||
|
* @param {Function} props.onVoteChange - 投票变化回调
|
||||||
|
*/
|
||||||
|
const EventEngagement = ({
|
||||||
|
eventId,
|
||||||
|
viewCount = 0,
|
||||||
|
followerCount = 0,
|
||||||
|
bullishCount = 0,
|
||||||
|
bearishCount = 0,
|
||||||
|
userVote = null,
|
||||||
|
size = 'sm',
|
||||||
|
showVoting = true,
|
||||||
|
onVoteChange,
|
||||||
|
}) => {
|
||||||
|
const toast = useToast();
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
|
// 本地状态管理(乐观更新)
|
||||||
|
const [localVote, setLocalVote] = useState(userVote);
|
||||||
|
const [localBullish, setLocalBullish] = useState(bullishCount);
|
||||||
|
const [localBearish, setLocalBearish] = useState(bearishCount);
|
||||||
|
const [isVoting, setIsVoting] = useState(false);
|
||||||
|
|
||||||
|
// 尺寸配置
|
||||||
|
const sizeConfig = {
|
||||||
|
xs: { fontSize: '10px', iconSize: '12px', spacing: 1, btnSize: 'xs' },
|
||||||
|
sm: { fontSize: 'xs', iconSize: '14px', spacing: 1.5, btnSize: 'xs' },
|
||||||
|
md: { fontSize: 'sm', iconSize: '16px', spacing: 2, btnSize: 'sm' },
|
||||||
|
};
|
||||||
|
const config = sizeConfig[size] || sizeConfig.sm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理投票
|
||||||
|
* @param {string} voteType - 'bullish' | 'bearish'
|
||||||
|
*/
|
||||||
|
const handleVote = useCallback(async (voteType) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
toast({
|
||||||
|
title: '请先登录',
|
||||||
|
description: '登录后才能参与投票',
|
||||||
|
status: 'warning',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVoting) return;
|
||||||
|
setIsVoting(true);
|
||||||
|
|
||||||
|
// 计算新的投票状态
|
||||||
|
const newVote = localVote === voteType ? null : voteType;
|
||||||
|
const oldVote = localVote;
|
||||||
|
|
||||||
|
// 乐观更新本地状态
|
||||||
|
setLocalVote(newVote);
|
||||||
|
|
||||||
|
// 更新计数
|
||||||
|
let newBullish = localBullish;
|
||||||
|
let newBearish = localBearish;
|
||||||
|
|
||||||
|
// 取消之前的投票
|
||||||
|
if (oldVote === 'bullish') newBullish--;
|
||||||
|
if (oldVote === 'bearish') newBearish--;
|
||||||
|
|
||||||
|
// 添加新投票
|
||||||
|
if (newVote === 'bullish') newBullish++;
|
||||||
|
if (newVote === 'bearish') newBearish++;
|
||||||
|
|
||||||
|
setLocalBullish(Math.max(0, newBullish));
|
||||||
|
setLocalBearish(Math.max(0, newBearish));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBase()}/api/events/${eventId}/sentiment-vote`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ vote_type: newVote }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
throw new Error(data.error || '投票失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用服务器返回的准确数据
|
||||||
|
if (data.data) {
|
||||||
|
setLocalBullish(data.data.bullish_count ?? newBullish);
|
||||||
|
setLocalBearish(data.data.bearish_count ?? newBearish);
|
||||||
|
setLocalVote(data.data.user_vote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
onVoteChange?.({
|
||||||
|
eventId,
|
||||||
|
userVote: data.data?.user_vote,
|
||||||
|
bullishCount: data.data?.bullish_count,
|
||||||
|
bearishCount: data.data?.bearish_count,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('投票失败:', error);
|
||||||
|
// 回滚状态
|
||||||
|
setLocalVote(oldVote);
|
||||||
|
setLocalBullish(bullishCount);
|
||||||
|
setLocalBearish(bearishCount);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '投票失败',
|
||||||
|
description: error.message,
|
||||||
|
status: 'error',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsVoting(false);
|
||||||
|
}
|
||||||
|
}, [eventId, isLoggedIn, isVoting, localVote, localBullish, localBearish, bullishCount, bearishCount, toast, onVoteChange]);
|
||||||
|
|
||||||
|
// 计算看多比例(用于显示)
|
||||||
|
const totalVotes = localBullish + localBearish;
|
||||||
|
const bullishRatio = totalVotes > 0 ? Math.round((localBullish / totalVotes) * 100) : 50;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack spacing={config.spacing} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{/* 浏览量 */}
|
||||||
|
<Tooltip label="浏览量" placement="top" hasArrow>
|
||||||
|
<HStack spacing={0.5} color="gray.400">
|
||||||
|
<ViewIcon boxSize={config.iconSize} />
|
||||||
|
<Text fontSize={config.fontSize}>{formatCount(viewCount)}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 关注数 */}
|
||||||
|
<Tooltip label="关注数" placement="top" hasArrow>
|
||||||
|
<HStack spacing={0.5} color="gray.400">
|
||||||
|
<StarIcon boxSize={config.iconSize} />
|
||||||
|
<Text fontSize={config.fontSize}>{formatCount(followerCount)}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 看多/看空投票 */}
|
||||||
|
{showVoting && (
|
||||||
|
<HStack spacing={0.5} ml={1}>
|
||||||
|
{/* 看多按钮 */}
|
||||||
|
<Tooltip label={localVote === 'bullish' ? '取消看多' : '看多'} placement="top" hasArrow>
|
||||||
|
<IconButton
|
||||||
|
size={config.btnSize}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="red"
|
||||||
|
icon={localVote === 'bullish'
|
||||||
|
? <TbArrowBigUpFilled size={config.iconSize} />
|
||||||
|
: <TbArrowBigUp size={config.iconSize} />
|
||||||
|
}
|
||||||
|
onClick={() => handleVote('bullish')}
|
||||||
|
isLoading={isVoting}
|
||||||
|
aria-label="看多"
|
||||||
|
minW="auto"
|
||||||
|
h="auto"
|
||||||
|
p={0.5}
|
||||||
|
color={localVote === 'bullish' ? 'red.400' : 'gray.400'}
|
||||||
|
_hover={{ color: 'red.400', bg: 'rgba(239, 68, 68, 0.1)' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 投票比例显示 */}
|
||||||
|
{totalVotes > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
label={`看多 ${localBullish} | 看空 ${localBearish}`}
|
||||||
|
placement="top"
|
||||||
|
hasArrow
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
fontSize={config.fontSize}
|
||||||
|
px={1}
|
||||||
|
py={0}
|
||||||
|
borderRadius="sm"
|
||||||
|
bg={bullishRatio >= 50 ? 'rgba(239, 68, 68, 0.15)' : 'rgba(16, 185, 129, 0.15)'}
|
||||||
|
color={bullishRatio >= 50 ? 'red.400' : 'green.400'}
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{bullishRatio}%
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 看空按钮 */}
|
||||||
|
<Tooltip label={localVote === 'bearish' ? '取消看空' : '看空'} placement="top" hasArrow>
|
||||||
|
<IconButton
|
||||||
|
size={config.btnSize}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="green"
|
||||||
|
icon={localVote === 'bearish'
|
||||||
|
? <TbArrowBigDownFilled size={config.iconSize} />
|
||||||
|
: <TbArrowBigDown size={config.iconSize} />
|
||||||
|
}
|
||||||
|
onClick={() => handleVote('bearish')}
|
||||||
|
isLoading={isVoting}
|
||||||
|
aria-label="看空"
|
||||||
|
minW="auto"
|
||||||
|
h="auto"
|
||||||
|
p={0.5}
|
||||||
|
color={localVote === 'bearish' ? 'green.400' : 'gray.400'}
|
||||||
|
_hover={{ color: 'green.400', bg: 'rgba(16, 185, 129, 0.1)' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventEngagement;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// 事件卡片原子组件
|
// 事件卡片原子组件
|
||||||
|
|
||||||
export { default as EventDescription } from './EventDescription';
|
export { default as EventDescription } from './EventDescription';
|
||||||
|
export { default as EventEngagement } from './EventEngagement';
|
||||||
export { default as EventFollowButton } from './EventFollowButton';
|
export { default as EventFollowButton } from './EventFollowButton';
|
||||||
export { default as EventHeader } from './EventHeader';
|
export { default as EventHeader } from './EventHeader';
|
||||||
export { default as EventImportanceBadge } from './EventImportanceBadge';
|
export { default as EventImportanceBadge } from './EventImportanceBadge';
|
||||||
|
|||||||
Reference in New Issue
Block a user