diff --git a/src/routes/lazy-components.js b/src/routes/lazy-components.js
index 48353be6..8e124a1a 100644
--- a/src/routes/lazy-components.js
+++ b/src/routes/lazy-components.js
@@ -42,6 +42,7 @@ export const lazyComponents = {
// 价值论坛模块
ValueForum: React.lazy(() => import('../views/ValueForum')),
ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')),
+ PredictionTopicDetail: React.lazy(() => import('../views/ValueForum/PredictionTopicDetail')),
// 数据浏览器模块
DataBrowser: React.lazy(() => import('../views/DataBrowser')),
diff --git a/src/routes/routeConfig.js b/src/routes/routeConfig.js
index 4f3911f1..3c0fe14a 100644
--- a/src/routes/routeConfig.js
+++ b/src/routes/routeConfig.js
@@ -181,6 +181,16 @@ export const routeConfig = [
description: '论坛帖子详细内容'
}
},
+ {
+ path: 'value-forum/prediction/:topicId',
+ component: lazyComponents.PredictionTopicDetail,
+ protection: PROTECTION_MODES.MODAL,
+ layout: 'main',
+ meta: {
+ title: '预测话题详情',
+ description: '预测市场话题详细信息'
+ }
+ },
// ==================== Agent模块 ====================
{
diff --git a/src/services/creditSystemService.js b/src/services/creditSystemService.js
new file mode 100644
index 00000000..e97fb6a2
--- /dev/null
+++ b/src/services/creditSystemService.js
@@ -0,0 +1,492 @@
+/**
+ * 积分系统服务
+ * 管理用户积分账户、交易、奖励等
+ */
+
+// ==================== 常量配置 ====================
+
+export const CREDIT_CONFIG = {
+ INITIAL_BALANCE: 10000, // 初始积分
+ MIN_BALANCE: 100, // 最低保留余额(破产保护)
+ MAX_SINGLE_BET: 1000, // 单次下注上限
+ DAILY_BONUS: 100, // 每日签到奖励
+ CREATE_TOPIC_COST: 100, // 创建话题费用
+};
+
+// 积分账户存储(生产环境应使用数据库)
+const userAccounts = new Map();
+
+// 交易记录存储
+const transactions = [];
+
+// ==================== 账户管理 ====================
+
+/**
+ * 获取用户账户
+ * @param {string} userId - 用户ID
+ * @returns {Object} 用户账户信息
+ */
+export const getUserAccount = (userId) => {
+ if (!userAccounts.has(userId)) {
+ // 首次访问,创建新账户
+ const newAccount = {
+ user_id: userId,
+ balance: CREDIT_CONFIG.INITIAL_BALANCE,
+ frozen: 0,
+ total: CREDIT_CONFIG.INITIAL_BALANCE,
+ total_earned: CREDIT_CONFIG.INITIAL_BALANCE,
+ total_spent: 0,
+ total_profit: 0,
+ active_positions: [],
+ stats: {
+ total_topics: 0,
+ win_count: 0,
+ loss_count: 0,
+ win_rate: 0,
+ best_profit: 0,
+ },
+ last_daily_bonus: null,
+ };
+ userAccounts.set(userId, newAccount);
+ }
+
+ return userAccounts.get(userId);
+};
+
+/**
+ * 更新用户账户
+ * @param {string} userId - 用户ID
+ * @param {Object} updates - 更新内容
+ */
+export const updateUserAccount = (userId, updates) => {
+ const account = getUserAccount(userId);
+ const updated = { ...account, ...updates };
+ userAccounts.set(userId, updated);
+ return updated;
+};
+
+/**
+ * 获取用户积分余额
+ * @param {string} userId - 用户ID
+ * @returns {number} 可用余额
+ */
+export const getBalance = (userId) => {
+ const account = getUserAccount(userId);
+ return account.balance;
+};
+
+/**
+ * 检查用户是否能支付
+ * @param {string} userId - 用户ID
+ * @param {number} amount - 金额
+ * @returns {boolean} 是否能支付
+ */
+export const canAfford = (userId, amount) => {
+ const account = getUserAccount(userId);
+ const afterBalance = account.balance - amount;
+
+ // 必须保留最低余额
+ return afterBalance >= CREDIT_CONFIG.MIN_BALANCE;
+};
+
+// ==================== 积分操作 ====================
+
+/**
+ * 增加积分
+ * @param {string} userId - 用户ID
+ * @param {number} amount - 金额
+ * @param {string} reason - 原因
+ */
+export const addCredits = (userId, amount, reason = '系统增加') => {
+ const account = getUserAccount(userId);
+
+ const updated = {
+ balance: account.balance + amount,
+ total: account.total + amount,
+ total_earned: account.total_earned + amount,
+ };
+
+ updateUserAccount(userId, updated);
+
+ // 记录交易
+ logTransaction({
+ user_id: userId,
+ type: 'earn',
+ amount,
+ reason,
+ balance_after: updated.balance,
+ });
+
+ return updated;
+};
+
+/**
+ * 扣除积分
+ * @param {string} userId - 用户ID
+ * @param {number} amount - 金额
+ * @param {string} reason - 原因
+ * @throws {Error} 如果余额不足
+ */
+export const deductCredits = (userId, amount, reason = '系统扣除') => {
+ if (!canAfford(userId, amount)) {
+ throw new Error(`积分不足,需要${amount}积分,但只有${getBalance(userId)}积分`);
+ }
+
+ const account = getUserAccount(userId);
+
+ const updated = {
+ balance: account.balance - amount,
+ total_spent: account.total_spent + amount,
+ };
+
+ updateUserAccount(userId, updated);
+
+ // 记录交易
+ logTransaction({
+ user_id: userId,
+ type: 'spend',
+ amount: -amount,
+ reason,
+ balance_after: updated.balance,
+ });
+
+ return updated;
+};
+
+/**
+ * 冻结积分(席位占用)
+ * @param {string} userId - 用户ID
+ * @param {number} amount - 金额
+ */
+export const freezeCredits = (userId, amount) => {
+ const account = getUserAccount(userId);
+
+ if (account.balance < amount) {
+ throw new Error('可用余额不足');
+ }
+
+ const updated = {
+ balance: account.balance - amount,
+ frozen: account.frozen + amount,
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+/**
+ * 解冻积分
+ * @param {string} userId - 用户ID
+ * @param {number} amount - 金额
+ */
+export const unfreezeCredits = (userId, amount) => {
+ const account = getUserAccount(userId);
+
+ const updated = {
+ balance: account.balance + amount,
+ frozen: account.frozen - amount,
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+// ==================== 每日奖励 ====================
+
+/**
+ * 领取每日签到奖励
+ * @param {string} userId - 用户ID
+ * @returns {Object} 奖励信息
+ */
+export const claimDailyBonus = (userId) => {
+ const account = getUserAccount(userId);
+ const today = new Date().toDateString();
+
+ // 检查是否已领取
+ if (account.last_daily_bonus === today) {
+ return {
+ success: false,
+ message: '今日已领取',
+ };
+ }
+
+ // 发放奖励
+ addCredits(userId, CREDIT_CONFIG.DAILY_BONUS, '每日签到');
+
+ // 更新领取时间
+ updateUserAccount(userId, { last_daily_bonus: today });
+
+ return {
+ success: true,
+ amount: CREDIT_CONFIG.DAILY_BONUS,
+ message: `获得${CREDIT_CONFIG.DAILY_BONUS}积分`,
+ };
+};
+
+/**
+ * 检查今天是否已签到
+ * @param {string} userId - 用户ID
+ * @returns {boolean}
+ */
+export const hasClaimedToday = (userId) => {
+ const account = getUserAccount(userId);
+ const today = new Date().toDateString();
+ return account.last_daily_bonus === today;
+};
+
+// ==================== 持仓管理 ====================
+
+/**
+ * 添加持仓
+ * @param {string} userId - 用户ID
+ * @param {Object} position - 持仓信息
+ */
+export const addPosition = (userId, position) => {
+ const account = getUserAccount(userId);
+
+ const updated = {
+ active_positions: [...account.active_positions, position],
+ stats: {
+ ...account.stats,
+ total_topics: account.stats.total_topics + 1,
+ },
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+/**
+ * 移除持仓
+ * @param {string} userId - 用户ID
+ * @param {string} positionId - 持仓ID
+ */
+export const removePosition = (userId, positionId) => {
+ const account = getUserAccount(userId);
+
+ const updated = {
+ active_positions: account.active_positions.filter((p) => p.id !== positionId),
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+/**
+ * 更新持仓
+ * @param {string} userId - 用户ID
+ * @param {string} positionId - 持仓ID
+ * @param {Object} updates - 更新内容
+ */
+export const updatePosition = (userId, positionId, updates) => {
+ const account = getUserAccount(userId);
+
+ const updated = {
+ active_positions: account.active_positions.map((p) =>
+ p.id === positionId ? { ...p, ...updates } : p
+ ),
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+/**
+ * 获取用户持仓
+ * @param {string} userId - 用户ID
+ * @param {string} topicId - 话题ID(可选)
+ * @returns {Array} 持仓列表
+ */
+export const getUserPositions = (userId, topicId = null) => {
+ const account = getUserAccount(userId);
+
+ if (topicId) {
+ return account.active_positions.filter((p) => p.topic_id === topicId);
+ }
+
+ return account.active_positions;
+};
+
+// ==================== 统计更新 ====================
+
+/**
+ * 记录胜利
+ * @param {string} userId - 用户ID
+ * @param {number} profit - 盈利金额
+ */
+export const recordWin = (userId, profit) => {
+ const account = getUserAccount(userId);
+
+ const newWinCount = account.stats.win_count + 1;
+ const totalGames = newWinCount + account.stats.loss_count;
+ const winRate = (newWinCount / totalGames) * 100;
+
+ const updated = {
+ total_profit: account.total_profit + profit,
+ stats: {
+ ...account.stats,
+ win_count: newWinCount,
+ win_rate: winRate,
+ best_profit: Math.max(account.stats.best_profit, profit),
+ },
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+/**
+ * 记录失败
+ * @param {string} userId - 用户ID
+ * @param {number} loss - 损失金额
+ */
+export const recordLoss = (userId, loss) => {
+ const account = getUserAccount(userId);
+
+ const newLossCount = account.stats.loss_count + 1;
+ const totalGames = account.stats.win_count + newLossCount;
+ const winRate = (account.stats.win_count / totalGames) * 100;
+
+ const updated = {
+ total_profit: account.total_profit - loss,
+ stats: {
+ ...account.stats,
+ loss_count: newLossCount,
+ win_rate: winRate,
+ },
+ };
+
+ updateUserAccount(userId, updated);
+ return updated;
+};
+
+// ==================== 排行榜 ====================
+
+/**
+ * 获取积分排行榜
+ * @param {number} limit - 返回数量
+ * @returns {Array} 排行榜数据
+ */
+export const getLeaderboard = (limit = 100) => {
+ const accounts = Array.from(userAccounts.values());
+
+ return accounts
+ .sort((a, b) => b.total - a.total)
+ .slice(0, limit)
+ .map((account, index) => ({
+ rank: index + 1,
+ user_id: account.user_id,
+ total: account.total,
+ total_profit: account.total_profit,
+ win_rate: account.stats.win_rate,
+ }));
+};
+
+/**
+ * 获取用户排名
+ * @param {string} userId - 用户ID
+ * @returns {number} 排名
+ */
+export const getUserRank = (userId) => {
+ const leaderboard = getLeaderboard(1000);
+ const index = leaderboard.findIndex((item) => item.user_id === userId);
+ return index >= 0 ? index + 1 : -1;
+};
+
+// ==================== 交易记录 ====================
+
+/**
+ * 记录交易
+ * @param {Object} transaction - 交易信息
+ */
+const logTransaction = (transaction) => {
+ const record = {
+ id: `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ timestamp: new Date().toISOString(),
+ ...transaction,
+ };
+
+ transactions.push(record);
+ return record;
+};
+
+/**
+ * 获取用户交易记录
+ * @param {string} userId - 用户ID
+ * @param {number} limit - 返回数量
+ * @returns {Array} 交易记录
+ */
+export const getUserTransactions = (userId, limit = 50) => {
+ return transactions
+ .filter((tx) => tx.user_id === userId)
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
+ .slice(0, limit);
+};
+
+// ==================== 批量操作 ====================
+
+/**
+ * 批量发放积分(如活动奖励)
+ * @param {Array} recipients - [{user_id, amount, reason}]
+ */
+export const batchAddCredits = (recipients) => {
+ const results = recipients.map(({ user_id, amount, reason }) => {
+ try {
+ return {
+ user_id,
+ success: true,
+ account: addCredits(user_id, amount, reason),
+ };
+ } catch (error) {
+ return {
+ user_id,
+ success: false,
+ error: error.message,
+ };
+ }
+ });
+
+ return results;
+};
+
+// ==================== 导出所有功能 ====================
+
+export default {
+ CREDIT_CONFIG,
+
+ // 账户管理
+ getUserAccount,
+ updateUserAccount,
+ getBalance,
+ canAfford,
+
+ // 积分操作
+ addCredits,
+ deductCredits,
+ freezeCredits,
+ unfreezeCredits,
+
+ // 每日奖励
+ claimDailyBonus,
+ hasClaimedToday,
+
+ // 持仓管理
+ addPosition,
+ removePosition,
+ updatePosition,
+ getUserPositions,
+
+ // 统计更新
+ recordWin,
+ recordLoss,
+
+ // 排行榜
+ getLeaderboard,
+ getUserRank,
+
+ // 交易记录
+ getUserTransactions,
+
+ // 批量操作
+ batchAddCredits,
+};
diff --git a/src/services/predictionMarketService.js b/src/services/predictionMarketService.js
new file mode 100644
index 00000000..25e943cd
--- /dev/null
+++ b/src/services/predictionMarketService.js
@@ -0,0 +1,738 @@
+/**
+ * 预测市场服务
+ * 核心功能:话题管理、席位交易、动态定价、领主系统、奖池分配
+ */
+
+import {
+ addCredits,
+ deductCredits,
+ canAfford,
+ addPosition,
+ removePosition,
+ updatePosition,
+ getUserPositions,
+ recordWin,
+ recordLoss,
+ CREDIT_CONFIG,
+} from './creditSystemService';
+
+// ==================== 常量配置 ====================
+
+export const MARKET_CONFIG = {
+ MAX_SEATS_PER_SIDE: 5, // 每个方向最多5个席位
+ TAX_RATE: 0.02, // 交易税率 2%
+ MIN_PRICE: 50, // 最低价格
+ MAX_PRICE: 950, // 最高价格
+ BASE_PRICE: 500, // 基础价格
+};
+
+// 话题存储(生产环境应使用Elasticsearch)
+const topics = new Map();
+
+// 席位存储
+const positions = new Map();
+
+// 交易记录
+const trades = [];
+
+// ==================== 动态定价算法 ====================
+
+/**
+ * 计算当前价格(简化版AMM)
+ * @param {number} yesShares - Yes方总份额
+ * @param {number} noShares - No方总份额
+ * @returns {Object} {yes: price, no: price}
+ */
+export const calculatePrice = (yesShares, noShares) => {
+ const totalShares = yesShares + noShares;
+
+ if (totalShares === 0) {
+ // 初始状态,双方价格相同
+ return {
+ yes: MARKET_CONFIG.BASE_PRICE,
+ no: MARKET_CONFIG.BASE_PRICE,
+ };
+ }
+
+ // 概率加权定价
+ const yesProb = yesShares / totalShares;
+ const noProb = noShares / totalShares;
+
+ // 价格 = 概率 * 1000,限制在 [MIN_PRICE, MAX_PRICE]
+ const yesPrice = Math.max(
+ MARKET_CONFIG.MIN_PRICE,
+ Math.min(MARKET_CONFIG.MAX_PRICE, yesProb * 1000)
+ );
+
+ const noPrice = Math.max(
+ MARKET_CONFIG.MIN_PRICE,
+ Math.min(MARKET_CONFIG.MAX_PRICE, noProb * 1000)
+ );
+
+ return { yes: yesPrice, no: noPrice };
+};
+
+/**
+ * 计算购买成本(含滑点)
+ * @param {number} currentShares - 当前份额
+ * @param {number} otherShares - 对手方份额
+ * @param {number} buyAmount - 购买数量
+ * @returns {number} 总成本
+ */
+export const calculateBuyCost = (currentShares, otherShares, buyAmount) => {
+ let totalCost = 0;
+ let tempShares = currentShares;
+
+ // 模拟逐步购买,累计成本
+ for (let i = 0; i < buyAmount; i++) {
+ tempShares += 1;
+ const prices = calculatePrice(tempShares, otherShares);
+ // 假设购买的是yes方
+ totalCost += prices.yes;
+ }
+
+ return totalCost;
+};
+
+/**
+ * 计算卖出收益(含滑点)
+ * @param {number} currentShares - 当前份额
+ * @param {number} otherShares - 对手方份额
+ * @param {number} sellAmount - 卖出数量
+ * @returns {number} 总收益
+ */
+export const calculateSellRevenue = (currentShares, otherShares, sellAmount) => {
+ let totalRevenue = 0;
+ let tempShares = currentShares;
+
+ // 模拟逐步卖出,累计收益
+ for (let i = 0; i < sellAmount; i++) {
+ const prices = calculatePrice(tempShares, otherShares);
+ totalRevenue += prices.yes;
+ tempShares -= 1;
+ }
+
+ return totalRevenue;
+};
+
+/**
+ * 计算交易税
+ * @param {number} amount - 交易金额
+ * @returns {number} 税费
+ */
+export const calculateTax = (amount) => {
+ return Math.floor(amount * MARKET_CONFIG.TAX_RATE);
+};
+
+// ==================== 话题管理 ====================
+
+/**
+ * 创建预测话题
+ * @param {Object} topicData - 话题数据
+ * @returns {Object} 创建的话题
+ */
+export const createTopic = (topicData) => {
+ const { author_id, title, description, category, tags, deadline, settlement_date } = topicData;
+
+ // 扣除创建费用
+ deductCredits(author_id, CREDIT_CONFIG.CREATE_TOPIC_COST, '创建预测话题');
+
+ const topic = {
+ id: `topic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ type: 'prediction',
+
+ // 基础信息
+ title,
+ description,
+ category,
+ tags: tags || [],
+
+ // 作者信息
+ author_id,
+ author_name: topicData.author_name,
+ author_avatar: topicData.author_avatar,
+
+ // 时间管理
+ created_at: new Date().toISOString(),
+ deadline: deadline || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 默认7天
+ settlement_date: settlement_date || new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(),
+ status: 'active',
+
+ // 预测选项
+ options: [
+ { id: 'yes', label: '看涨 / Yes', color: '#48BB78' },
+ { id: 'no', label: '看跌 / No', color: '#F56565' },
+ ],
+
+ // 市场数据
+ total_pool: CREDIT_CONFIG.CREATE_TOPIC_COST, // 创建费用进入奖池
+ tax_rate: MARKET_CONFIG.TAX_RATE,
+
+ // 席位数据
+ positions: {
+ yes: {
+ seats: [],
+ total_shares: 0,
+ current_price: MARKET_CONFIG.BASE_PRICE,
+ lord_id: null,
+ },
+ no: {
+ seats: [],
+ total_shares: 0,
+ current_price: MARKET_CONFIG.BASE_PRICE,
+ lord_id: null,
+ },
+ },
+
+ // 交易统计
+ stats: {
+ total_volume: 0,
+ total_transactions: 0,
+ unique_traders: new Set(),
+ },
+
+ // 结果
+ settlement: {
+ result: null,
+ evidence: null,
+ settled_by: null,
+ settled_at: null,
+ },
+ };
+
+ topics.set(topic.id, topic);
+ return topic;
+};
+
+/**
+ * 获取话题详情
+ * @param {string} topicId - 话题ID
+ * @returns {Object} 话题详情
+ */
+export const getTopic = (topicId) => {
+ return topics.get(topicId);
+};
+
+/**
+ * 更新话题
+ * @param {string} topicId - 话题ID
+ * @param {Object} updates - 更新内容
+ */
+export const updateTopic = (topicId, updates) => {
+ const topic = getTopic(topicId);
+ const updated = { ...topic, ...updates };
+ topics.set(topicId, updated);
+ return updated;
+};
+
+/**
+ * 获取所有话题列表
+ * @param {Object} filters - 筛选条件
+ * @returns {Array} 话题列表
+ */
+export const getTopics = (filters = {}) => {
+ let topicList = Array.from(topics.values());
+
+ // 按状态筛选
+ if (filters.status) {
+ topicList = topicList.filter((t) => t.status === filters.status);
+ }
+
+ // 按分类筛选
+ if (filters.category) {
+ topicList = topicList.filter((t) => t.category === filters.category);
+ }
+
+ // 排序
+ const sortBy = filters.sortBy || 'created_at';
+ topicList.sort((a, b) => {
+ if (sortBy === 'created_at') {
+ return new Date(b.created_at) - new Date(a.created_at);
+ }
+ if (sortBy === 'total_pool') {
+ return b.total_pool - a.total_pool;
+ }
+ if (sortBy === 'total_volume') {
+ return b.stats.total_volume - a.stats.total_volume;
+ }
+ return 0;
+ });
+
+ return topicList;
+};
+
+// ==================== 席位管理 ====================
+
+/**
+ * 创建席位
+ * @param {Object} positionData - 席位数据
+ * @returns {Object} 创建的席位
+ */
+const createPosition = (positionData) => {
+ const position = {
+ id: `pos_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ ...positionData,
+ acquired_at: new Date().toISOString(),
+ last_traded_at: new Date().toISOString(),
+ is_lord: false,
+ };
+
+ positions.set(position.id, position);
+ return position;
+};
+
+/**
+ * 获取席位
+ * @param {string} positionId - 席位ID
+ * @returns {Object} 席位信息
+ */
+export const getPosition = (positionId) => {
+ return positions.get(positionId);
+};
+
+/**
+ * 分配席位(取份额最高的前5名)
+ * @param {Array} allPositions - 所有持仓
+ * @returns {Array} 席位列表
+ */
+const allocateSeats = (allPositions) => {
+ // 按份额排序
+ const sorted = [...allPositions].sort((a, b) => b.shares - a.shares);
+
+ // 取前5名
+ return sorted.slice(0, MARKET_CONFIG.MAX_SEATS_PER_SIDE);
+};
+
+/**
+ * 确定领主(份额最多的人)
+ * @param {Array} seats - 席位列表
+ * @returns {string|null} 领主用户ID
+ */
+const determineLord = (seats) => {
+ if (seats.length === 0) return null;
+
+ const lord = seats.reduce((max, seat) => (seat.shares > max.shares ? seat : max));
+
+ return lord.holder_id;
+};
+
+/**
+ * 更新领主标识
+ * @param {string} topicId - 话题ID
+ * @param {string} optionId - 选项ID
+ */
+const updateLordStatus = (topicId, optionId) => {
+ const topic = getTopic(topicId);
+ const sideData = topic.positions[optionId];
+
+ // 重新分配席位
+ const allPositions = Array.from(positions.values()).filter(
+ (p) => p.topic_id === topicId && p.option_id === optionId
+ );
+
+ const seats = allocateSeats(allPositions);
+ const lordId = determineLord(seats);
+
+ // 更新所有席位的领主标识
+ allPositions.forEach((position) => {
+ const isLord = position.holder_id === lordId;
+ positions.set(position.id, { ...position, is_lord: isLord });
+ });
+
+ // 更新话题数据
+ updateTopic(topicId, {
+ positions: {
+ ...topic.positions,
+ [optionId]: {
+ ...sideData,
+ seats,
+ lord_id: lordId,
+ },
+ },
+ });
+
+ return lordId;
+};
+
+// ==================== 交易执行 ====================
+
+/**
+ * 购买席位
+ * @param {Object} tradeData - 交易数据
+ * @returns {Object} 交易结果
+ */
+export const buyPosition = (tradeData) => {
+ const { user_id, user_name, user_avatar, topic_id, option_id, shares } = tradeData;
+
+ // 验证
+ const topic = getTopic(topic_id);
+ if (!topic) throw new Error('话题不存在');
+ if (topic.status !== 'active') throw new Error('话题已关闭交易');
+ if (topic.author_id === user_id) throw new Error('不能参与自己发起的话题');
+
+ // 检查购买上限
+ if (shares * MARKET_CONFIG.BASE_PRICE > CREDIT_CONFIG.MAX_SINGLE_BET) {
+ throw new Error(`单次购买上限为${CREDIT_CONFIG.MAX_SINGLE_BET}积分`);
+ }
+
+ // 获取当前市场数据
+ const sideData = topic.positions[option_id];
+ const otherOptionId = option_id === 'yes' ? 'no' : 'yes';
+ const otherSideData = topic.positions[otherOptionId];
+
+ // 计算成本
+ const cost = calculateBuyCost(sideData.total_shares, otherSideData.total_shares, shares);
+ const tax = calculateTax(cost);
+ const totalCost = cost + tax;
+
+ // 检查余额
+ if (!canAfford(user_id, totalCost)) {
+ throw new Error(`积分不足,需要${totalCost}积分`);
+ }
+
+ // 扣除积分
+ deductCredits(user_id, totalCost, `购买预测席位 - ${topic.title}`);
+
+ // 税费进入奖池
+ updateTopic(topic_id, {
+ total_pool: topic.total_pool + tax,
+ stats: {
+ ...topic.stats,
+ total_volume: topic.stats.total_volume + totalCost,
+ total_transactions: topic.stats.total_transactions + 1,
+ unique_traders: topic.stats.unique_traders.add(user_id),
+ },
+ });
+
+ // 查找用户是否已有该选项的席位
+ let userPosition = Array.from(positions.values()).find(
+ (p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id
+ );
+
+ if (userPosition) {
+ // 更新现有席位
+ const newShares = userPosition.shares + shares;
+ const newAvgCost = (userPosition.avg_cost * userPosition.shares + cost) / newShares;
+
+ positions.set(userPosition.id, {
+ ...userPosition,
+ shares: newShares,
+ avg_cost: newAvgCost,
+ last_traded_at: new Date().toISOString(),
+ });
+
+ // 更新用户账户持仓
+ updatePosition(user_id, userPosition.id, {
+ shares: newShares,
+ avg_cost: newAvgCost,
+ });
+ } else {
+ // 创建新席位
+ const newPosition = createPosition({
+ topic_id,
+ option_id,
+ holder_id: user_id,
+ holder_name: user_name,
+ holder_avatar: user_avatar,
+ shares,
+ avg_cost: cost / shares,
+ current_value: cost,
+ unrealized_pnl: 0,
+ });
+
+ // 添加到用户账户
+ addPosition(user_id, {
+ id: newPosition.id,
+ topic_id,
+ option_id,
+ shares,
+ avg_cost: cost / shares,
+ });
+
+ userPosition = newPosition;
+ }
+
+ // 更新话题席位数据
+ updateTopic(topic_id, {
+ positions: {
+ ...topic.positions,
+ [option_id]: {
+ ...sideData,
+ total_shares: sideData.total_shares + shares,
+ },
+ },
+ });
+
+ // 更新价格
+ const newPrices = calculatePrice(
+ topic.positions[option_id].total_shares + shares,
+ topic.positions[otherOptionId].total_shares
+ );
+
+ updateTopic(topic_id, {
+ positions: {
+ ...topic.positions,
+ yes: { ...topic.positions.yes, current_price: newPrices.yes },
+ no: { ...topic.positions.no, current_price: newPrices.no },
+ },
+ });
+
+ // 更新领主状态
+ const newLordId = updateLordStatus(topic_id, option_id);
+
+ // 记录交易
+ const trade = {
+ id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ topic_id,
+ option_id,
+ buyer_id: user_id,
+ seller_id: null,
+ type: 'buy',
+ shares,
+ price: cost / shares,
+ total_cost: totalCost,
+ tax,
+ created_at: new Date().toISOString(),
+ };
+ trades.push(trade);
+
+ return {
+ success: true,
+ position: userPosition,
+ trade,
+ new_lord_id: newLordId,
+ current_price: newPrices[option_id],
+ };
+};
+
+/**
+ * 卖出席位
+ * @param {Object} tradeData - 交易数据
+ * @returns {Object} 交易结果
+ */
+export const sellPosition = (tradeData) => {
+ const { user_id, topic_id, option_id, shares } = tradeData;
+
+ // 验证
+ const topic = getTopic(topic_id);
+ if (!topic) throw new Error('话题不存在');
+ if (topic.status !== 'active') throw new Error('话题已关闭交易');
+
+ // 查找用户席位
+ const userPosition = Array.from(positions.values()).find(
+ (p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id
+ );
+
+ if (!userPosition) throw new Error('未持有该席位');
+ if (userPosition.shares < shares) throw new Error('持有份额不足');
+
+ // 获取当前市场数据
+ const sideData = topic.positions[option_id];
+ const otherOptionId = option_id === 'yes' ? 'no' : 'yes';
+ const otherSideData = topic.positions[otherOptionId];
+
+ // 计算收益
+ const revenue = calculateSellRevenue(sideData.total_shares, otherSideData.total_shares, shares);
+ const tax = calculateTax(revenue);
+ const netRevenue = revenue - tax;
+
+ // 返还积分
+ addCredits(user_id, netRevenue, `卖出预测席位 - ${topic.title}`);
+
+ // 税费进入奖池
+ updateTopic(topic_id, {
+ total_pool: topic.total_pool + tax,
+ stats: {
+ ...topic.stats,
+ total_volume: topic.stats.total_volume + revenue,
+ total_transactions: topic.stats.total_transactions + 1,
+ },
+ });
+
+ // 更新席位
+ const newShares = userPosition.shares - shares;
+
+ if (newShares === 0) {
+ // 完全卖出,删除席位
+ positions.delete(userPosition.id);
+ removePosition(user_id, userPosition.id);
+ } else {
+ // 部分卖出,更新份额
+ positions.set(userPosition.id, {
+ ...userPosition,
+ shares: newShares,
+ last_traded_at: new Date().toISOString(),
+ });
+
+ updatePosition(user_id, userPosition.id, { shares: newShares });
+ }
+
+ // 更新话题席位数据
+ updateTopic(topic_id, {
+ positions: {
+ ...topic.positions,
+ [option_id]: {
+ ...sideData,
+ total_shares: sideData.total_shares - shares,
+ },
+ },
+ });
+
+ // 更新价格
+ const newPrices = calculatePrice(
+ topic.positions[option_id].total_shares - shares,
+ topic.positions[otherOptionId].total_shares
+ );
+
+ updateTopic(topic_id, {
+ positions: {
+ ...topic.positions,
+ yes: { ...topic.positions.yes, current_price: newPrices.yes },
+ no: { ...topic.positions.no, current_price: newPrices.no },
+ },
+ });
+
+ // 更新领主状态
+ const newLordId = updateLordStatus(topic_id, option_id);
+
+ // 记录交易
+ const trade = {
+ id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ topic_id,
+ option_id,
+ buyer_id: null,
+ seller_id: user_id,
+ type: 'sell',
+ shares,
+ price: revenue / shares,
+ total_cost: netRevenue,
+ tax,
+ created_at: new Date().toISOString(),
+ };
+ trades.push(trade);
+
+ return {
+ success: true,
+ trade,
+ new_lord_id: newLordId,
+ current_price: newPrices[option_id],
+ };
+};
+
+// ==================== 结算 ====================
+
+/**
+ * 结算话题
+ * @param {string} topicId - 话题ID
+ * @param {string} result - 结果 'yes' | 'no'
+ * @param {string} evidence - 证据说明
+ * @param {string} settledBy - 裁决者ID
+ * @returns {Object} 结算结果
+ */
+export const settleTopic = (topicId, result, evidence, settledBy) => {
+ const topic = getTopic(topicId);
+
+ if (!topic) throw new Error('话题不存在');
+ if (topic.status === 'settled') throw new Error('话题已结算');
+
+ // 只有作者可以结算
+ if (topic.author_id !== settledBy) throw new Error('无权结算');
+
+ // 获取获胜方和失败方
+ const winningOption = result;
+ const losingOption = result === 'yes' ? 'no' : 'yes';
+
+ const winners = Array.from(positions.values()).filter(
+ (p) => p.topic_id === topicId && p.option_id === winningOption
+ );
+
+ const losers = Array.from(positions.values()).filter(
+ (p) => p.topic_id === topicId && p.option_id === losingOption
+ );
+
+ // 分配奖池
+ if (winners.length === 0) {
+ // 无人获胜,奖池返还给作者
+ addCredits(topic.author_id, topic.total_pool, '话题奖池返还');
+ } else {
+ // 计算获胜方总份额
+ const totalWinningShares = winners.reduce((sum, p) => sum + p.shares, 0);
+
+ // 按份额分配
+ winners.forEach((position) => {
+ const share = position.shares / totalWinningShares;
+ const reward = Math.floor(topic.total_pool * share);
+
+ // 返还本金 + 奖池分成
+ const refund = Math.floor(position.avg_cost * position.shares);
+ const total = refund + reward;
+
+ addCredits(position.holder_id, total, `预测获胜 - ${topic.title}`);
+
+ // 记录胜利
+ recordWin(position.holder_id, reward);
+
+ // 删除席位
+ positions.delete(position.id);
+ removePosition(position.holder_id, position.id);
+ });
+ }
+
+ // 失败方损失本金
+ losers.forEach((position) => {
+ const loss = Math.floor(position.avg_cost * position.shares);
+
+ // 记录失败
+ recordLoss(position.holder_id, loss);
+
+ // 删除席位
+ positions.delete(position.id);
+ removePosition(position.holder_id, position.id);
+ });
+
+ // 更新话题状态
+ updateTopic(topicId, {
+ status: 'settled',
+ settlement: {
+ result,
+ evidence,
+ settled_by: settledBy,
+ settled_at: new Date().toISOString(),
+ },
+ });
+
+ return {
+ success: true,
+ winners_count: winners.length,
+ losers_count: losers.length,
+ total_distributed: topic.total_pool,
+ };
+};
+
+// ==================== 数据导出 ====================
+
+export default {
+ MARKET_CONFIG,
+
+ // 定价算法
+ calculatePrice,
+ calculateBuyCost,
+ calculateSellRevenue,
+ calculateTax,
+
+ // 话题管理
+ createTopic,
+ getTopic,
+ updateTopic,
+ getTopics,
+
+ // 席位管理
+ getPosition,
+
+ // 交易
+ buyPosition,
+ sellPosition,
+
+ // 结算
+ settleTopic,
+};
diff --git a/src/views/ValueForum/PredictionTopicDetail.js b/src/views/ValueForum/PredictionTopicDetail.js
new file mode 100644
index 00000000..3c031869
--- /dev/null
+++ b/src/views/ValueForum/PredictionTopicDetail.js
@@ -0,0 +1,532 @@
+/**
+ * 预测话题详情页
+ * 展示预测市场的完整信息、交易、评论等
+ */
+
+import React, { useState, useEffect } from 'react';
+import {
+ Box,
+ Container,
+ Heading,
+ Text,
+ Button,
+ HStack,
+ VStack,
+ Flex,
+ Badge,
+ Avatar,
+ Icon,
+ Progress,
+ Divider,
+ useDisclosure,
+ useToast,
+ SimpleGrid,
+} from '@chakra-ui/react';
+import {
+ TrendingUp,
+ TrendingDown,
+ Crown,
+ Users,
+ Clock,
+ DollarSign,
+ ShoppingCart,
+ ArrowLeftRight,
+ CheckCircle2,
+} from 'lucide-react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { motion } from 'framer-motion';
+import { forumColors } from '@theme/forumTheme';
+import { getTopic } from '@services/predictionMarketService';
+import { getUserAccount } from '@services/creditSystemService';
+import { useAuth } from '@contexts/AuthContext';
+import TradeModal from './components/TradeModal';
+
+const MotionBox = motion(Box);
+
+const PredictionTopicDetail = () => {
+ const { topicId } = useParams();
+ const navigate = useNavigate();
+ const toast = useToast();
+ const { user } = useAuth();
+
+ // 状态
+ const [topic, setTopic] = useState(null);
+ const [userAccount, setUserAccount] = useState(null);
+ const [tradeMode, setTradeMode] = useState('buy');
+
+ // 模态框
+ const {
+ isOpen: isTradeModalOpen,
+ onOpen: onTradeModalOpen,
+ onClose: onTradeModalClose,
+ } = useDisclosure();
+
+ // 加载话题数据
+ useEffect(() => {
+ const loadTopic = () => {
+ const topicData = getTopic(topicId);
+ if (topicData) {
+ setTopic(topicData);
+ } else {
+ toast({
+ title: '话题不存在',
+ status: 'error',
+ duration: 3000,
+ });
+ navigate('/value-forum');
+ }
+ };
+
+ loadTopic();
+
+ if (user) {
+ setUserAccount(getUserAccount(user.id));
+ }
+ }, [topicId, user]);
+
+ // 打开交易弹窗
+ const handleOpenTrade = (mode) => {
+ if (!user) {
+ toast({
+ title: '请先登录',
+ status: 'warning',
+ duration: 3000,
+ });
+ return;
+ }
+
+ setTradeMode(mode);
+ onTradeModalOpen();
+ };
+
+ // 交易成功回调
+ const handleTradeSuccess = () => {
+ // 刷新话题数据
+ setTopic(getTopic(topicId));
+ setUserAccount(getUserAccount(user.id));
+ };
+
+ if (!topic) {
+ return null;
+ }
+
+ // 获取选项数据
+ const yesData = topic.positions?.yes || { total_shares: 0, current_price: 500, lord_id: null };
+ const noData = topic.positions?.no || { total_shares: 0, current_price: 500, lord_id: null };
+
+ // 计算总份额
+ const totalShares = yesData.total_shares + noData.total_shares;
+
+ // 计算百分比
+ const yesPercent = totalShares > 0 ? (yesData.total_shares / totalShares) * 100 : 50;
+ const noPercent = totalShares > 0 ? (noData.total_shares / totalShares) * 100 : 50;
+
+ // 格式化时间
+ const formatTime = (dateString) => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diff = date - now;
+
+ const days = Math.floor(diff / 86400000);
+ const hours = Math.floor(diff / 3600000);
+
+ if (days > 0) return `${days}天后`;
+ if (hours > 0) return `${hours}小时后`;
+ return '即将截止';
+ };
+
+ return (
+