update pay function
This commit is contained in:
836
prediction_market_routes_to_add.py
Normal file
836
prediction_market_routes_to_add.py
Normal file
@@ -0,0 +1,836 @@
|
||||
# ===========================
|
||||
# 预测市场 API 路由
|
||||
# 请将此文件内容插入到 app.py 的 `if __name__ == '__main__':` 之前
|
||||
# ===========================
|
||||
|
||||
# --- 积分系统 API ---
|
||||
|
||||
@app.route('/api/prediction/credit/account', methods=['GET'])
|
||||
@login_required
|
||||
def get_credit_account():
|
||||
"""获取用户积分账户"""
|
||||
try:
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
|
||||
# 如果账户不存在,自动创建
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'balance': float(account.balance),
|
||||
'frozen_balance': float(account.frozen_balance),
|
||||
'available_balance': float(account.balance - account.frozen_balance),
|
||||
'total_earned': float(account.total_earned),
|
||||
'total_spent': float(account.total_spent),
|
||||
'last_daily_bonus_at': account.last_daily_bonus_at.isoformat() if account.last_daily_bonus_at else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/credit/daily-bonus', methods=['POST'])
|
||||
@login_required
|
||||
def claim_daily_bonus():
|
||||
"""领取每日奖励(100积分)"""
|
||||
try:
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
|
||||
# 检查是否已领取今日奖励
|
||||
today = beijing_now().date()
|
||||
if account.last_daily_bonus_at and account.last_daily_bonus_at.date() == today:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '今日奖励已领取'
|
||||
}), 400
|
||||
|
||||
# 发放奖励
|
||||
bonus_amount = 100.0
|
||||
account.balance += bonus_amount
|
||||
account.total_earned += bonus_amount
|
||||
account.last_daily_bonus_at = beijing_now()
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='daily_bonus',
|
||||
amount=bonus_amount,
|
||||
balance_after=account.balance,
|
||||
description='每日登录奖励'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'领取成功,获得 {bonus_amount} 积分',
|
||||
'data': {
|
||||
'bonus_amount': bonus_amount,
|
||||
'new_balance': float(account.balance)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 预测话题 API ---
|
||||
|
||||
@app.route('/api/prediction/topics', methods=['POST'])
|
||||
@login_required
|
||||
def create_prediction_topic():
|
||||
"""创建预测话题(消耗100积分)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
title = data.get('title', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
category = data.get('category', 'stock')
|
||||
deadline_str = data.get('deadline')
|
||||
|
||||
# 验证参数
|
||||
if not title or len(title) < 5:
|
||||
return jsonify({'success': False, 'error': '标题至少5个字符'}), 400
|
||||
|
||||
if not deadline_str:
|
||||
return jsonify({'success': False, 'error': '请设置截止时间'}), 400
|
||||
|
||||
# 解析截止时间
|
||||
deadline = datetime.fromisoformat(deadline_str.replace('Z', '+00:00'))
|
||||
if deadline <= beijing_now():
|
||||
return jsonify({'success': False, 'error': '截止时间必须在未来'}), 400
|
||||
|
||||
# 检查积分账户
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
if not account or account.balance < 100:
|
||||
return jsonify({'success': False, 'error': '积分不足(需要100积分)'}), 400
|
||||
|
||||
# 扣除创建费用
|
||||
create_cost = 100.0
|
||||
account.balance -= create_cost
|
||||
account.total_spent += create_cost
|
||||
|
||||
# 创建话题
|
||||
topic = PredictionTopic(
|
||||
creator_id=current_user.id,
|
||||
title=title,
|
||||
description=description,
|
||||
category=category,
|
||||
deadline=deadline
|
||||
)
|
||||
db.session.add(topic)
|
||||
|
||||
# 记录积分交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='create_topic',
|
||||
amount=-create_cost,
|
||||
balance_after=account.balance,
|
||||
description=f'创建预测话题:{title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '话题创建成功',
|
||||
'data': {
|
||||
'topic_id': topic.id,
|
||||
'title': topic.title,
|
||||
'new_balance': float(account.balance)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics', methods=['GET'])
|
||||
def get_prediction_topics():
|
||||
"""获取预测话题列表"""
|
||||
try:
|
||||
status = request.args.get('status', 'active')
|
||||
category = request.args.get('category')
|
||||
sort_by = request.args.get('sort_by', 'created_at')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# 构建查询
|
||||
query = PredictionTopic.query
|
||||
|
||||
if status:
|
||||
query = query.filter_by(status=status)
|
||||
if category:
|
||||
query = query.filter_by(category=category)
|
||||
|
||||
# 排序
|
||||
if sort_by == 'hot':
|
||||
query = query.order_by(desc(PredictionTopic.views_count))
|
||||
elif sort_by == 'participants':
|
||||
query = query.order_by(desc(PredictionTopic.participants_count))
|
||||
else:
|
||||
query = query.order_by(desc(PredictionTopic.created_at))
|
||||
|
||||
# 分页
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
topics = pagination.items
|
||||
|
||||
# 格式化返回数据
|
||||
topics_data = []
|
||||
for topic in topics:
|
||||
# 计算市场倾向
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
|
||||
|
||||
topics_data.append({
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'description': topic.description,
|
||||
'category': topic.category,
|
||||
'status': topic.status,
|
||||
'yes_price': float(topic.yes_price),
|
||||
'no_price': float(topic.no_price),
|
||||
'yes_probability': round(yes_prob, 1),
|
||||
'total_pool': float(topic.total_pool),
|
||||
'yes_lord': {
|
||||
'id': topic.yes_lord.id,
|
||||
'username': topic.yes_lord.username,
|
||||
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
|
||||
'avatar_url': topic.yes_lord.avatar_url
|
||||
} if topic.yes_lord else None,
|
||||
'no_lord': {
|
||||
'id': topic.no_lord.id,
|
||||
'username': topic.no_lord.username,
|
||||
'nickname': topic.no_lord.nickname or topic.no_lord.username,
|
||||
'avatar_url': topic.no_lord.avatar_url
|
||||
} if topic.no_lord else None,
|
||||
'deadline': topic.deadline.isoformat(),
|
||||
'created_at': topic.created_at.isoformat(),
|
||||
'views_count': topic.views_count,
|
||||
'comments_count': topic.comments_count,
|
||||
'participants_count': topic.participants_count,
|
||||
'creator': {
|
||||
'id': topic.creator.id,
|
||||
'username': topic.creator.username,
|
||||
'nickname': topic.creator.nickname or topic.creator.username
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': topics_data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages,
|
||||
'has_next': pagination.has_next,
|
||||
'has_prev': pagination.has_prev
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>', methods=['GET'])
|
||||
def get_prediction_topic_detail(topic_id):
|
||||
"""获取预测话题详情"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
# 增加浏览量
|
||||
topic.views_count += 1
|
||||
db.session.commit()
|
||||
|
||||
# 计算市场倾向
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
|
||||
|
||||
# 获取 TOP 5 持仓(YES 和 NO 各5个)
|
||||
yes_top_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='yes'
|
||||
).order_by(desc(PredictionPosition.shares)).limit(5).all()
|
||||
|
||||
no_top_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='no'
|
||||
).order_by(desc(PredictionPosition.shares)).limit(5).all()
|
||||
|
||||
def format_position(position):
|
||||
return {
|
||||
'user': {
|
||||
'id': position.user.id,
|
||||
'username': position.user.username,
|
||||
'nickname': position.user.nickname or position.user.username,
|
||||
'avatar_url': position.user.avatar_url
|
||||
},
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost),
|
||||
'total_invested': float(position.total_invested),
|
||||
'is_lord': (topic.yes_lord_id == position.user_id and position.direction == 'yes') or
|
||||
(topic.no_lord_id == position.user_id and position.direction == 'no')
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'description': topic.description,
|
||||
'category': topic.category,
|
||||
'status': topic.status,
|
||||
'result': topic.result,
|
||||
'yes_price': float(topic.yes_price),
|
||||
'no_price': float(topic.no_price),
|
||||
'yes_total_shares': topic.yes_total_shares,
|
||||
'no_total_shares': topic.no_total_shares,
|
||||
'yes_probability': round(yes_prob, 1),
|
||||
'no_probability': round(100 - yes_prob, 1),
|
||||
'total_pool': float(topic.total_pool),
|
||||
'yes_lord': {
|
||||
'id': topic.yes_lord.id,
|
||||
'username': topic.yes_lord.username,
|
||||
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
|
||||
'avatar_url': topic.yes_lord.avatar_url
|
||||
} if topic.yes_lord else None,
|
||||
'no_lord': {
|
||||
'id': topic.no_lord.id,
|
||||
'username': topic.no_lord.username,
|
||||
'nickname': topic.no_lord.nickname or topic.no_lord.username,
|
||||
'avatar_url': topic.no_lord.avatar_url
|
||||
} if topic.no_lord else None,
|
||||
'yes_top_positions': [format_position(p) for p in yes_top_positions],
|
||||
'no_top_positions': [format_position(p) for p in no_top_positions],
|
||||
'deadline': topic.deadline.isoformat(),
|
||||
'settled_at': topic.settled_at.isoformat() if topic.settled_at else None,
|
||||
'created_at': topic.created_at.isoformat(),
|
||||
'views_count': topic.views_count,
|
||||
'comments_count': topic.comments_count,
|
||||
'participants_count': topic.participants_count,
|
||||
'creator': {
|
||||
'id': topic.creator.id,
|
||||
'username': topic.creator.username,
|
||||
'nickname': topic.creator.nickname or topic.creator.username,
|
||||
'avatar_url': topic.creator.avatar_url
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/settle', methods=['POST'])
|
||||
@login_required
|
||||
def settle_prediction_topic(topic_id):
|
||||
"""结算预测话题(仅创建者可操作)"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
# 验证权限
|
||||
if topic.creator_id != current_user.id:
|
||||
return jsonify({'success': False, 'error': '只有创建者可以结算'}), 403
|
||||
|
||||
# 验证状态
|
||||
if topic.status != 'active':
|
||||
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
|
||||
|
||||
# 验证截止时间
|
||||
if beijing_now() < topic.deadline:
|
||||
return jsonify({'success': False, 'error': '未到截止时间'}), 400
|
||||
|
||||
# 获取结算结果
|
||||
data = request.get_json()
|
||||
result = data.get('result') # 'yes', 'no', 'draw'
|
||||
|
||||
if result not in ['yes', 'no', 'draw']:
|
||||
return jsonify({'success': False, 'error': '无效的结算结果'}), 400
|
||||
|
||||
# 更新话题状态
|
||||
topic.status = 'settled'
|
||||
topic.result = result
|
||||
topic.settled_at = beijing_now()
|
||||
|
||||
# 获取获胜方的所有持仓
|
||||
if result == 'draw':
|
||||
# 平局:所有人按投入比例分配奖池
|
||||
all_positions = PredictionPosition.query.filter_by(topic_id=topic_id).all()
|
||||
total_invested = sum(p.total_invested for p in all_positions)
|
||||
|
||||
for position in all_positions:
|
||||
if total_invested > 0:
|
||||
share_ratio = position.total_invested / total_invested
|
||||
prize = topic.total_pool * share_ratio
|
||||
|
||||
# 发放奖金
|
||||
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
|
||||
if account:
|
||||
account.balance += prize
|
||||
account.total_earned += prize
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=position.user_id,
|
||||
transaction_type='settle_win',
|
||||
amount=prize,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
description=f'预测平局,获得奖池分红:{topic.title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
else:
|
||||
# YES 或 NO 获胜
|
||||
winning_direction = result
|
||||
winning_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction=winning_direction
|
||||
).all()
|
||||
|
||||
if winning_positions:
|
||||
total_winning_shares = sum(p.shares for p in winning_positions)
|
||||
|
||||
for position in winning_positions:
|
||||
# 按份额比例分配奖池
|
||||
share_ratio = position.shares / total_winning_shares
|
||||
prize = topic.total_pool * share_ratio
|
||||
|
||||
# 发放奖金
|
||||
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
|
||||
if account:
|
||||
account.balance += prize
|
||||
account.total_earned += prize
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=position.user_id,
|
||||
transaction_type='settle_win',
|
||||
amount=prize,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
description=f'预测正确,获得奖金:{topic.title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'话题已结算,结果为:{result}',
|
||||
'data': {
|
||||
'topic_id': topic.id,
|
||||
'result': result,
|
||||
'total_pool': float(topic.total_pool),
|
||||
'settled_at': topic.settled_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 交易 API ---
|
||||
|
||||
@app.route('/api/prediction/trade/buy', methods=['POST'])
|
||||
@login_required
|
||||
def buy_prediction_shares():
|
||||
"""买入预测份额"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
topic_id = data.get('topic_id')
|
||||
direction = data.get('direction') # 'yes' or 'no'
|
||||
shares = data.get('shares', 0)
|
||||
|
||||
# 验证参数
|
||||
if not topic_id or direction not in ['yes', 'no'] or shares <= 0:
|
||||
return jsonify({'success': False, 'error': '参数错误'}), 400
|
||||
|
||||
if shares > 1000:
|
||||
return jsonify({'success': False, 'error': '单次最多买入1000份额'}), 400
|
||||
|
||||
# 获取话题
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
if topic.status != 'active':
|
||||
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
|
||||
|
||||
if beijing_now() >= topic.deadline:
|
||||
return jsonify({'success': False, 'error': '话题已截止'}), 400
|
||||
|
||||
# 获取积分账户
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
db.session.flush()
|
||||
|
||||
# 计算价格
|
||||
current_price = topic.yes_price if direction == 'yes' else topic.no_price
|
||||
|
||||
# 简化的AMM定价:price = (对应方份额 / 总份额) * 1000
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
if total_shares > 0:
|
||||
if direction == 'yes':
|
||||
current_price = (topic.yes_total_shares / total_shares) * 1000
|
||||
else:
|
||||
current_price = (topic.no_total_shares / total_shares) * 1000
|
||||
else:
|
||||
current_price = 500.0 # 初始价格
|
||||
|
||||
# 买入后价格会上涨,使用平均价格
|
||||
after_total = total_shares + shares
|
||||
if direction == 'yes':
|
||||
after_yes_shares = topic.yes_total_shares + shares
|
||||
after_price = (after_yes_shares / after_total) * 1000
|
||||
else:
|
||||
after_no_shares = topic.no_total_shares + shares
|
||||
after_price = (after_no_shares / after_total) * 1000
|
||||
|
||||
avg_price = (current_price + after_price) / 2
|
||||
|
||||
# 计算费用
|
||||
amount = avg_price * shares
|
||||
tax = amount * 0.02 # 2% 手续费
|
||||
total_cost = amount + tax
|
||||
|
||||
# 检查余额
|
||||
if account.balance < total_cost:
|
||||
return jsonify({'success': False, 'error': '积分不足'}), 400
|
||||
|
||||
# 扣除费用
|
||||
account.balance -= total_cost
|
||||
account.total_spent += total_cost
|
||||
|
||||
# 更新话题数据
|
||||
if direction == 'yes':
|
||||
topic.yes_total_shares += shares
|
||||
topic.yes_price = after_price
|
||||
else:
|
||||
topic.no_total_shares += shares
|
||||
topic.no_price = after_price
|
||||
|
||||
topic.total_pool += tax # 手续费进入奖池
|
||||
|
||||
# 更新或创建持仓
|
||||
position = PredictionPosition.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
direction=direction
|
||||
).first()
|
||||
|
||||
if position:
|
||||
# 更新平均成本
|
||||
old_cost = position.avg_cost * position.shares
|
||||
new_cost = avg_price * shares
|
||||
position.shares += shares
|
||||
position.avg_cost = (old_cost + new_cost) / position.shares
|
||||
position.total_invested += total_cost
|
||||
else:
|
||||
position = PredictionPosition(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
direction=direction,
|
||||
shares=shares,
|
||||
avg_cost=avg_price,
|
||||
total_invested=total_cost
|
||||
)
|
||||
db.session.add(position)
|
||||
topic.participants_count += 1
|
||||
|
||||
# 更新领主
|
||||
if direction == 'yes':
|
||||
# 找到YES方持仓最多的用户
|
||||
top_yes = db.session.query(PredictionPosition).filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='yes'
|
||||
).order_by(desc(PredictionPosition.shares)).first()
|
||||
if top_yes:
|
||||
topic.yes_lord_id = top_yes.user_id
|
||||
else:
|
||||
# 找到NO方持仓最多的用户
|
||||
top_no = db.session.query(PredictionPosition).filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='no'
|
||||
).order_by(desc(PredictionPosition.shares)).first()
|
||||
if top_no:
|
||||
topic.no_lord_id = top_no.user_id
|
||||
|
||||
# 记录交易
|
||||
transaction = PredictionTransaction(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
transaction_type='buy',
|
||||
direction=direction,
|
||||
shares=shares,
|
||||
price=avg_price,
|
||||
amount=amount,
|
||||
tax=tax,
|
||||
total_cost=total_cost
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
# 记录积分交易
|
||||
credit_transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='prediction_buy',
|
||||
amount=-total_cost,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
related_transaction_id=transaction.id,
|
||||
description=f'买入 {direction.upper()} 份额:{topic.title}'
|
||||
)
|
||||
db.session.add(credit_transaction)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '买入成功',
|
||||
'data': {
|
||||
'transaction_id': transaction.id,
|
||||
'shares': shares,
|
||||
'price': round(avg_price, 2),
|
||||
'total_cost': round(total_cost, 2),
|
||||
'tax': round(tax, 2),
|
||||
'new_balance': float(account.balance),
|
||||
'new_position': {
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/positions', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_positions():
|
||||
"""获取用户的所有持仓"""
|
||||
try:
|
||||
positions = PredictionPosition.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
positions_data = []
|
||||
for position in positions:
|
||||
topic = position.topic
|
||||
|
||||
# 计算当前市值(如果话题还在进行中)
|
||||
current_value = 0
|
||||
profit = 0
|
||||
profit_rate = 0
|
||||
|
||||
if topic.status == 'active':
|
||||
current_price = topic.yes_price if position.direction == 'yes' else topic.no_price
|
||||
current_value = current_price * position.shares
|
||||
profit = current_value - position.total_invested
|
||||
profit_rate = (profit / position.total_invested * 100) if position.total_invested > 0 else 0
|
||||
|
||||
positions_data.append({
|
||||
'id': position.id,
|
||||
'topic': {
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'status': topic.status,
|
||||
'result': topic.result,
|
||||
'deadline': topic.deadline.isoformat()
|
||||
},
|
||||
'direction': position.direction,
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost),
|
||||
'total_invested': float(position.total_invested),
|
||||
'current_value': round(current_value, 2),
|
||||
'profit': round(profit, 2),
|
||||
'profit_rate': round(profit_rate, 2),
|
||||
'created_at': position.created_at.isoformat(),
|
||||
'is_lord': (topic.yes_lord_id == current_user.id and position.direction == 'yes') or
|
||||
(topic.no_lord_id == current_user.id and position.direction == 'no')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': positions_data,
|
||||
'count': len(positions_data)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 评论 API ---
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['POST'])
|
||||
@login_required
|
||||
def create_topic_comment(topic_id):
|
||||
"""发表话题评论"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
data = request.get_json()
|
||||
content = data.get('content', '').strip()
|
||||
parent_id = data.get('parent_id')
|
||||
|
||||
if not content or len(content) < 2:
|
||||
return jsonify({'success': False, 'error': '评论内容至少2个字符'}), 400
|
||||
|
||||
# 创建评论
|
||||
comment = TopicComment(
|
||||
topic_id=topic_id,
|
||||
user_id=current_user.id,
|
||||
content=content,
|
||||
parent_id=parent_id
|
||||
)
|
||||
|
||||
# 如果是领主评论,自动置顶
|
||||
is_lord = (topic.yes_lord_id == current_user.id) or (topic.no_lord_id == current_user.id)
|
||||
if is_lord:
|
||||
comment.is_pinned = True
|
||||
|
||||
db.session.add(comment)
|
||||
|
||||
# 更新话题评论数
|
||||
topic.comments_count += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '评论成功',
|
||||
'data': {
|
||||
'comment_id': comment.id,
|
||||
'content': comment.content,
|
||||
'is_pinned': comment.is_pinned,
|
||||
'created_at': comment.created_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['GET'])
|
||||
def get_topic_comments(topic_id):
|
||||
"""获取话题评论列表"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# 置顶评论在前,然后按时间倒序
|
||||
query = TopicComment.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
status='active',
|
||||
parent_id=None # 只获取顶级评论
|
||||
).order_by(
|
||||
desc(TopicComment.is_pinned),
|
||||
desc(TopicComment.created_at)
|
||||
)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
comments = pagination.items
|
||||
|
||||
def format_comment(comment):
|
||||
# 获取回复
|
||||
replies = TopicComment.query.filter_by(
|
||||
parent_id=comment.id,
|
||||
status='active'
|
||||
).order_by(TopicComment.created_at).limit(5).all()
|
||||
|
||||
return {
|
||||
'id': comment.id,
|
||||
'content': comment.content,
|
||||
'is_pinned': comment.is_pinned,
|
||||
'likes_count': comment.likes_count,
|
||||
'created_at': comment.created_at.isoformat(),
|
||||
'user': {
|
||||
'id': comment.user.id,
|
||||
'username': comment.user.username,
|
||||
'nickname': comment.user.nickname or comment.user.username,
|
||||
'avatar_url': comment.user.avatar_url
|
||||
},
|
||||
'is_lord': (topic.yes_lord_id == comment.user_id) or (topic.no_lord_id == comment.user_id),
|
||||
'replies': [{
|
||||
'id': reply.id,
|
||||
'content': reply.content,
|
||||
'created_at': reply.created_at.isoformat(),
|
||||
'user': {
|
||||
'id': reply.user.id,
|
||||
'username': reply.user.username,
|
||||
'nickname': reply.user.nickname or reply.user.username,
|
||||
'avatar_url': reply.user.avatar_url
|
||||
}
|
||||
} for reply in replies]
|
||||
}
|
||||
|
||||
comments_data = [format_comment(comment) for comment in comments]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': comments_data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/comments/<int:comment_id>/like', methods=['POST'])
|
||||
@login_required
|
||||
def like_topic_comment(comment_id):
|
||||
"""点赞/取消点赞评论"""
|
||||
try:
|
||||
comment = TopicComment.query.get_or_404(comment_id)
|
||||
|
||||
# 检查是否已点赞
|
||||
existing_like = TopicCommentLike.query.filter_by(
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# 取消点赞
|
||||
db.session.delete(existing_like)
|
||||
comment.likes_count = max(0, comment.likes_count - 1)
|
||||
action = 'unliked'
|
||||
else:
|
||||
# 点赞
|
||||
like = TopicCommentLike(
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(like)
|
||||
comment.likes_count += 1
|
||||
action = 'liked'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'action': action,
|
||||
'likes_count': comment.likes_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
713
src/views/ValueForum/components/PredictionGuideModal.js
Normal file
713
src/views/ValueForum/components/PredictionGuideModal.js
Normal file
@@ -0,0 +1,713 @@
|
||||
/**
|
||||
* 预测市场玩法说明模态框
|
||||
* 帮助用户理解游戏规则和操作流程
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Badge,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListIcon,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Image,
|
||||
SimpleGrid,
|
||||
Flex,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Zap,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Crown,
|
||||
DollarSign,
|
||||
Users,
|
||||
Clock,
|
||||
Trophy,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
Target,
|
||||
Wallet,
|
||||
RefreshCw,
|
||||
Gift,
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const PredictionGuideModal = ({ isOpen, onClose }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
// 特性列表
|
||||
const features = [
|
||||
{
|
||||
icon: Zap,
|
||||
title: '预测市场',
|
||||
desc: '对未来事件进行预测,购买看涨/看跌席位',
|
||||
color: 'yellow.400',
|
||||
},
|
||||
{
|
||||
icon: DollarSign,
|
||||
title: '虚拟积分',
|
||||
desc: '使用论坛积分参与,初始10,000积分',
|
||||
color: 'green.400',
|
||||
},
|
||||
{
|
||||
icon: Crown,
|
||||
title: '领主系统',
|
||||
desc: '持有最多份额者成为领主,拥有特权',
|
||||
color: 'orange.400',
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: '赢取奖池',
|
||||
desc: '预测正确者分享奖池,最高收益无上限',
|
||||
color: 'purple.400',
|
||||
},
|
||||
];
|
||||
|
||||
// 操作步骤
|
||||
const steps = [
|
||||
{
|
||||
step: 1,
|
||||
title: '发起预测话题',
|
||||
desc: '创建一个可预测的话题,设置截止时间',
|
||||
icon: Zap,
|
||||
details: [
|
||||
'点击"发起预测"按钮',
|
||||
'填写话题标题和描述',
|
||||
'选择分类和截止时间',
|
||||
'支付100积分创建费用(进入奖池)',
|
||||
],
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: '购买席位表达观点',
|
||||
desc: '选择看涨或看跌方向,购买席位',
|
||||
icon: TrendingUp,
|
||||
details: [
|
||||
'浏览预测话题,选择感兴趣的',
|
||||
'点击"购买席位"',
|
||||
'选择方向:看涨(Yes) 或 看跌(No)',
|
||||
'调整购买份额(1-10份)',
|
||||
'支付积分(含2%交易税)',
|
||||
],
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: '成为领主获得特权',
|
||||
desc: '持有最多份额自动成为领主',
|
||||
icon: Crown,
|
||||
details: [
|
||||
'持有某方向最多份额即成为领主',
|
||||
'领主评论自动置顶(计划中)',
|
||||
'专属👑领主徽章显示',
|
||||
'领主身份随交易实时变更',
|
||||
],
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: '等待结果揭晓',
|
||||
desc: '话题到期后,作者提交结果',
|
||||
icon: Clock,
|
||||
details: [
|
||||
'话题到达截止时间,停止交易',
|
||||
'作者提交结果和证据',
|
||||
'系统自动计算奖池分配',
|
||||
'获胜方按份额比例分享奖池',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 常见问题
|
||||
const faqs = [
|
||||
{
|
||||
q: '💰 初始积分是多少?会破产吗?',
|
||||
a: '每个用户初始获得10,000积分。系统有破产保护机制,最低保留100积分,不会完全归零。每日签到可获得100积分奖励。',
|
||||
},
|
||||
{
|
||||
q: '📊 价格是如何变动的?',
|
||||
a: '价格根据供需关系动态调整。购买某方向会导致该方向价格上涨,对方价格下跌。采用简化版AMM(自动做市商)算法。',
|
||||
},
|
||||
{
|
||||
q: '👑 领主有什么特权?',
|
||||
a: '领主是持有某方向最多份额的用户。特权包括:评论自动置顶(计划中)、专属徽章显示、更高的话语权。领主身份随交易实时变更。',
|
||||
},
|
||||
{
|
||||
q: '🎯 如何计算收益?',
|
||||
a: '获胜方按持有份额比例分享奖池。例如:奖池1000积分,你持有获胜方30%份额,则获得300积分 + 返还本金。',
|
||||
},
|
||||
{
|
||||
q: '💸 交易税去哪了?',
|
||||
a: '每笔交易收取2%交易税,全部进入话题奖池。这确保了即使无人参与对立面,奖池也有资金可分配。',
|
||||
},
|
||||
{
|
||||
q: '🔄 可以卖出席位吗?',
|
||||
a: '可以!在话题截止前,随时可以卖出席位。卖出价格同样根据市场供需动态计算,可能高于或低于买入价。',
|
||||
},
|
||||
{
|
||||
q: '⚖️ 如何确保公平?',
|
||||
a: '作者不能参与自己发起的话题。结果需要提交证据。未来将引入社区投票仲裁机制处理争议。',
|
||||
},
|
||||
{
|
||||
q: '📈 单次最多能买多少?',
|
||||
a: '单次购买上限1000积分,避免大户操纵市场。每个方向最多5个席位持有者,确保市场活跃度。',
|
||||
},
|
||||
];
|
||||
|
||||
// 示例场景
|
||||
const exampleScenario = {
|
||||
title: '📖 完整示例:贵州茅台预测',
|
||||
steps: [
|
||||
{
|
||||
title: '1. 创建话题',
|
||||
content: '小明创建话题:"贵州茅台下周会涨吗?",支付100积分。奖池:100积分。',
|
||||
},
|
||||
{
|
||||
title: '2. 用户参与',
|
||||
content: [
|
||||
'小红购买3份"看涨",花费1,530积分(含税)',
|
||||
'小李购买2份"看跌",花费1,020积分(含税)',
|
||||
'交易税进入奖池:51积分',
|
||||
'当前奖池:151积分',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '3. 价格变动',
|
||||
content: '看涨份额增加 → 看涨价格上涨至680积分/份,看跌价格下跌至320积分/份',
|
||||
},
|
||||
{
|
||||
title: '4. 领主诞生',
|
||||
content: '小红持有看涨方最多份额,成为看涨方👑领主',
|
||||
},
|
||||
{
|
||||
title: '5. 结果揭晓',
|
||||
content: [
|
||||
'一周后,茅台上涨5%,小明提交结果:"看涨"获胜',
|
||||
'小红获得:本金1500积分 + 奖池151积分 = 1651积分',
|
||||
'小红净收益:1651 - 1530 = +121积分 (收益率7.9%)',
|
||||
'小李损失本金1000积分',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="6xl" isCentered scrollBehavior="inside">
|
||||
<ModalOverlay backdropFilter="blur(4px)" bg="blackAlpha.800" />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="2xl"
|
||||
border="2px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
maxH="90vh"
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
borderTopRadius="2xl"
|
||||
py="6"
|
||||
>
|
||||
<HStack spacing="3">
|
||||
<Icon as={Zap} boxSize="28px" color={forumColors.background.main} />
|
||||
<VStack align="start" spacing="0">
|
||||
<Text fontSize="2xl" fontWeight="bold" color={forumColors.background.main}>
|
||||
预测市场玩法说明
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.background.main} opacity={0.9}>
|
||||
5分钟快速上手,开启预测之旅
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.background.main} top="6" right="6" />
|
||||
|
||||
<ModalBody py="6">
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={setActiveTab}
|
||||
variant="soft-rounded"
|
||||
colorScheme="yellow"
|
||||
>
|
||||
<TabList mb="6" flexWrap="wrap">
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={Target} boxSize="16px" mr="2" />
|
||||
核心玩法
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={CheckCircle2} boxSize="16px" mr="2" />
|
||||
操作步骤
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={AlertCircle} boxSize="16px" mr="2" />
|
||||
常见问题
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<Icon as={Trophy} boxSize="16px" mr="2" />
|
||||
示例演示
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* 核心玩法 */}
|
||||
<TabPanel p="0">
|
||||
<VStack spacing="6" align="stretch">
|
||||
{/* 核心特性 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing="4">
|
||||
{features.map((feature, index) => (
|
||||
<MotionBox
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
_hover={{
|
||||
borderColor: forumColors.border.gold,
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: forumColors.shadows.gold,
|
||||
}}
|
||||
>
|
||||
<HStack spacing="4" align="start">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="3"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<Icon as={feature.icon} boxSize="24px" color={feature.color} />
|
||||
</Box>
|
||||
<VStack align="start" spacing="1" flex="1">
|
||||
<Text fontWeight="700" fontSize="lg" color={forumColors.text.primary}>
|
||||
{feature.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{feature.desc}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</MotionBox>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider borderColor={forumColors.border.default} />
|
||||
|
||||
{/* 游戏规则 */}
|
||||
<Box>
|
||||
<Text fontSize="xl" fontWeight="700" color={forumColors.text.primary} mb="4">
|
||||
📜 游戏规则
|
||||
</Text>
|
||||
<VStack spacing="3" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<HStack spacing="2" mb="2">
|
||||
<Icon as={Wallet} boxSize="18px" color={forumColors.primary[500]} />
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
积分系统
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
初始积分:10,000积分(免费领取)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
最低保留:100积分(破产保护)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
单次上限:1,000积分(防止操纵)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
每日签到:+100积分(每天领取)
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="2">
|
||||
<Icon as={TrendingUp} boxSize="18px" color="green.400" />
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
双向市场
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
看涨 (Yes):预测会上涨/发生
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
看跌 (No):预测会下跌/不发生
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
价格动态变化,根据供需关系调整
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
可以随时买入和卖出(截止前)
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="2">
|
||||
<Icon as={Crown} boxSize="18px" color="yellow.400" />
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
领主系统
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
持有最多份额者自动成为领主
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
领主评论自动置顶(计划中)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
专属👑徽章和金色边框
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
领主身份随交易实时变更
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
p="4"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2" mb="2">
|
||||
<Icon as={Trophy} boxSize="18px" color="purple.400" />
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
奖池分配
|
||||
</Text>
|
||||
</HStack>
|
||||
<List spacing="2" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
交易税:每笔交易2%进入奖池
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
获胜方按份额比例分享奖池
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
返还本金 + 奖池分成
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListIcon as={CheckCircle2} color="green.400" />
|
||||
失败方损失本金
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 操作步骤 */}
|
||||
<TabPanel p="0">
|
||||
<VStack spacing="6" align="stretch">
|
||||
{steps.map((step, index) => (
|
||||
<MotionBox
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
bg={forumColors.background.hover}
|
||||
p="6"
|
||||
borderRadius="xl"
|
||||
border="2px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
_hover={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: forumColors.shadows.gold,
|
||||
}}
|
||||
>
|
||||
{/* 步骤编号 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10px"
|
||||
right="-10px"
|
||||
fontSize="120px"
|
||||
fontWeight="900"
|
||||
color={forumColors.border.default}
|
||||
opacity={0.1}
|
||||
lineHeight="1"
|
||||
>
|
||||
{step.step}
|
||||
</Box>
|
||||
|
||||
<HStack spacing="4" align="start" mb="4" position="relative" zIndex={1}>
|
||||
<Box
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
p="3"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<Icon as={step.icon} boxSize="24px" color={forumColors.background.main} />
|
||||
</Box>
|
||||
|
||||
<VStack align="start" spacing="1" flex="1">
|
||||
<HStack>
|
||||
<Badge
|
||||
bg={forumColors.primary[500]}
|
||||
color={forumColors.background.main}
|
||||
px="3"
|
||||
py="1"
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
>
|
||||
步骤 {step.step}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xl" fontWeight="700" color={forumColors.text.primary}>
|
||||
{step.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{step.desc}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
<Divider borderColor={forumColors.border.default} mb="4" />
|
||||
|
||||
<List spacing="2" pl="4">
|
||||
{step.details.map((detail, i) => (
|
||||
<ListItem
|
||||
key={i}
|
||||
fontSize="sm"
|
||||
color={forumColors.text.secondary}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text>{detail}</Text>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</MotionBox>
|
||||
))}
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 常见问题 */}
|
||||
<TabPanel p="0">
|
||||
<Accordion allowMultiple>
|
||||
{faqs.map((faq, index) => (
|
||||
<AccordionItem
|
||||
key={index}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
borderRadius="lg"
|
||||
mb="3"
|
||||
bg={forumColors.background.hover}
|
||||
>
|
||||
<AccordionButton
|
||||
py="4"
|
||||
_hover={{ bg: forumColors.background.card }}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<Box flex="1" textAlign="left">
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{faq.q}
|
||||
</Text>
|
||||
</Box>
|
||||
<AccordionIcon color={forumColors.primary[500]} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb="4">
|
||||
<Text fontSize="sm" color={forumColors.text.secondary} lineHeight="1.8">
|
||||
{faq.a}
|
||||
</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</TabPanel>
|
||||
|
||||
{/* 示例演示 */}
|
||||
<TabPanel p="0">
|
||||
<VStack spacing="6" align="stretch">
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
p="6"
|
||||
borderRadius="xl"
|
||||
border="2px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
>
|
||||
<Text fontSize="2xl" fontWeight="700" color={forumColors.text.primary} mb="2">
|
||||
{exampleScenario.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
通过完整案例理解预测市场的运作流程
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{exampleScenario.steps.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
bg={forumColors.background.hover}
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontWeight="700" color={forumColors.primary[500]} mb="3">
|
||||
{item.title}
|
||||
</Text>
|
||||
{Array.isArray(item.content) ? (
|
||||
<VStack spacing="2" align="stretch">
|
||||
{item.content.map((line, i) => (
|
||||
<HStack key={i} spacing="2">
|
||||
<Icon as={ArrowRight} boxSize="14px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{line}
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{item.content}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* 关键提示 */}
|
||||
<Box
|
||||
bg="green.50"
|
||||
border="2px solid"
|
||||
borderColor="green.400"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack spacing="3" mb="3">
|
||||
<Icon as={Gift} boxSize="24px" color="green.600" />
|
||||
<Text fontWeight="700" color="green.700" fontSize="lg">
|
||||
💡 关键提示
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 早期参与价格低,后期参与价格高
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 可以通过"低买高卖"赚取差价(Flipper玩法)
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 即使预测失败,也可能通过卖出获利
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.700">
|
||||
✅ 成为领主可获得社区影响力
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
borderTop="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py="4"
|
||||
>
|
||||
<HStack spacing="3" w="full" justify="space-between">
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>
|
||||
💡 提示:每日签到可获得100积分
|
||||
</Text>
|
||||
<Button
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
fontWeight="bold"
|
||||
onClick={onClose}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
>
|
||||
开始预测
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionGuideModal;
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
TabPanel,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { Search, PenSquare, TrendingUp, Clock, Heart, Zap } from 'lucide-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';
|
||||
@@ -38,6 +38,7 @@ 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);
|
||||
|
||||
@@ -54,6 +55,7 @@ const ValueForum = () => {
|
||||
|
||||
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) => {
|
||||
@@ -178,6 +180,24 @@ const ValueForum = () => {
|
||||
|
||||
{/* 发帖按钮 */}
|
||||
<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}
|
||||
@@ -462,6 +482,12 @@ const ValueForum = () => {
|
||||
onClose={onPredictionModalClose}
|
||||
onTopicCreated={handlePredictionCreated}
|
||||
/>
|
||||
|
||||
{/* 玩法说明模态框 */}
|
||||
<PredictionGuideModal
|
||||
isOpen={isGuideModalOpen}
|
||||
onClose={onGuideModalClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user