diff --git a/src/mocks/data/forum.js b/src/mocks/data/forum.js new file mode 100644 index 00000000..12aff576 --- /dev/null +++ b/src/mocks/data/forum.js @@ -0,0 +1,394 @@ +/** + * 价值论坛帖子 Mock 数据 + */ + +// 模拟用户 +export const mockForumUsers = [ + { + id: "user_1", + nickname: "价值投资者", + username: "value_investor", + avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + }, + { + id: "user_2", + nickname: "趋势猎手", + username: "trend_hunter", + avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2", + }, + { + id: "user_3", + nickname: "量化先锋", + username: "quant_pioneer", + avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3", + }, + { + id: "user_4", + nickname: "股市老兵", + username: "stock_veteran", + avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + }, + { + id: "user_5", + nickname: "新手小白", + username: "newbie", + avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=newbie5", + }, +]; + +// 帖子标签 +export const mockTags = [ + "A股", + "美股", + "港股", + "新能源", + "半导体", + "AI", + "消费", + "医药", + "金融", + "地产", + "白酒", + "锂电池", + "光伏", + "汽车", + "军工", +]; + +// 帖子分类 +export const mockCategories = [ + "analysis", // 个股分析 + "strategy", // 投资策略 + "news", // 市场资讯 + "discussion", // 讨论交流 + "experience", // 经验分享 +]; + +// 模拟帖子列表 +export const mockPosts = [ + { + id: "post_001", + author_id: "user_1", + author_name: "价值投资者", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + title: "深度解析:宁德时代2024年业绩展望", + content: + "宁德时代作为全球动力电池龙头,2024年面临几个关键变量:\n\n1. **产能扩张**:公司在欧洲、北美的产能布局持续推进\n2. **技术迭代**:麒麟电池、钠离子电池等新技术商业化进度\n3. **竞争格局**:比亚迪、中创新航等竞争对手的市场份额变化\n\n从估值角度看,当前PE约25倍,处于历史中位数附近...", + images: [ + "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800", + "https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?w=800", + ], + tags: ["新能源", "锂电池", "A股"], + category: "analysis", + likes_count: 156, + comments_count: 42, + views_count: 2345, + created_at: "2024-12-20T10:30:00Z", + updated_at: "2024-12-20T10:30:00Z", + is_pinned: true, + status: "active", + }, + { + id: "post_002", + author_id: "user_2", + author_name: "趋势猎手", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2", + title: "技术面分析:上证指数短期走势预判", + content: + "从技术形态来看,上证指数近期呈现以下特征:\n\n- MACD金叉形成,多头趋势确立\n- 5日均线上穿10日均线\n- 成交量温和放大\n\n综合来看,短期内大盘有望向3200点发起冲击,支撑位在3050点附近。操作上建议逢低布局优质赛道龙头。", + images: [ + "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800", + ], + tags: ["A股", "技术分析"], + category: "strategy", + likes_count: 89, + comments_count: 28, + views_count: 1567, + created_at: "2024-12-19T15:20:00Z", + updated_at: "2024-12-19T15:20:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_003", + author_id: "user_3", + author_name: "量化先锋", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3", + title: "AI芯片赛道深度研究:英伟达vs AMD", + content: + "随着ChatGPT引爆AI浪潮,算力需求爆发式增长。本文对比分析两大芯片巨头:\n\n**英伟达 (NVDA)**\n- GPU市场份额超80%\n- CUDA生态护城河深厚\n- 数据中心业务高速增长\n\n**AMD**\n- MI300系列强势追赶\n- 性价比优势明显\n- 客户多元化策略\n\n投资建议:长期看好英伟达,但AMD估值更具吸引力...", + images: [ + "https://images.unsplash.com/photo-1518770660439-4636190af475?w=800", + "https://images.unsplash.com/photo-1555255707-c07966088b7b?w=800", + ], + tags: ["美股", "AI", "半导体"], + category: "analysis", + likes_count: 234, + comments_count: 67, + views_count: 4521, + created_at: "2024-12-18T09:00:00Z", + updated_at: "2024-12-18T09:00:00Z", + is_pinned: true, + status: "active", + }, + { + id: "post_004", + author_id: "user_4", + author_name: "股市老兵", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + title: "白酒板块投资逻辑:消费复苏下的机会", + content: + "白酒作为A股核心资产,具有以下投资逻辑:\n\n1. 消费升级持续,高端白酒需求稳定\n2. 品牌壁垒高,提价能力强\n3. 现金流充沛,分红率提升空间大\n\n重点关注标的:\n- 贵州茅台:行业龙头,确定性最强\n- 五粮液:次高端领军,性价比突出\n- 山西汾酒:清香型龙头,成长性好", + images: [], + tags: ["白酒", "消费", "A股"], + category: "analysis", + likes_count: 112, + comments_count: 35, + views_count: 1890, + created_at: "2024-12-17T14:30:00Z", + updated_at: "2024-12-17T14:30:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_005", + author_id: "user_5", + author_name: "新手小白", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=newbie5", + title: "请教:如何判断一只股票是否值得长期持有?", + content: + "刚入市不久,想请教各位大佬几个问题:\n\n1. 判断一家公司是否值得长期投资,最重要的指标是什么?\n2. PE、PB这些估值指标应该怎么用?\n3. 行业龙头和细分赛道龙头怎么选?\n\n希望大家不吝赐教,感谢!", + images: [], + tags: ["投资入门", "讨论"], + category: "discussion", + likes_count: 45, + comments_count: 52, + views_count: 876, + created_at: "2024-12-16T11:00:00Z", + updated_at: "2024-12-16T11:00:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_006", + author_id: "user_1", + author_name: "价值投资者", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + title: "医药板块复盘:集采影响逐步消化", + content: + "经过近两年的调整,医药板块估值已回到合理区间。从政策面看:\n\n- 集采常态化,边际影响减弱\n- 创新药审批加速\n- 医保谈判规则趋于稳定\n\n建议关注方向:\n1. 创新药龙头(恒瑞医药、百济神州)\n2. CXO赛道(药明康德、康龙化成)\n3. 医疗器械(迈瑞医疗、联影医疗)", + images: [ + "https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=800", + ], + tags: ["医药", "A股"], + category: "analysis", + likes_count: 78, + comments_count: 23, + views_count: 1234, + created_at: "2024-12-15T16:45:00Z", + updated_at: "2024-12-15T16:45:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_007", + author_id: "user_2", + author_name: "趋势猎手", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2", + title: "港股科技股投资机会分析", + content: + "港股科技股经历大幅调整后,估值已具吸引力:\n\n**腾讯控股**\n- 游戏业务恢复增长\n- 视频号商业化提速\n- 回购力度加大\n\n**阿里巴巴**\n- 云业务盈利改善\n- 电商份额企稳\n- 分拆上市预期\n\n**美团**\n- 外卖业务利润率提升\n- 即时零售空间广阔", + images: [], + tags: ["港股", "科技", "互联网"], + category: "analysis", + likes_count: 145, + comments_count: 41, + views_count: 2156, + created_at: "2024-12-14T10:15:00Z", + updated_at: "2024-12-14T10:15:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_008", + author_id: "user_3", + author_name: "量化先锋", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3", + title: "光伏行业2024年展望:产能出清进行时", + content: + "光伏行业正经历产能过剩带来的调整期:\n\n**现状分析**\n- 硅料价格跌至成本线附近\n- 组件企业盈利承压\n- 落后产能加速出清\n\n**未来展望**\n- N型技术渗透率提升\n- 海外需求持续高增\n- 龙头市占率提升\n\n投资建议:等待产业链利润重新分配,关注技术领先的龙头企业。", + images: [ + "https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800", + "https://images.unsplash.com/photo-1558449028-b53a39d100fc?w=800", + ], + tags: ["光伏", "新能源", "A股"], + category: "analysis", + likes_count: 167, + comments_count: 48, + views_count: 2890, + created_at: "2024-12-13T08:30:00Z", + updated_at: "2024-12-13T08:30:00Z", + is_pinned: false, + status: "active", + }, + { + id: "post_009", + author_id: "user_4", + author_name: "股市老兵", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + title: "分享我的投资框架:价值与成长的平衡", + content: + "投资十五年,总结出一套适合自己的投资框架:\n\n**选股标准**\n1. ROE连续5年>15%\n2. 资产负债率<50%\n3. 经营性现金流为正\n4. 行业地位前三\n\n**估值方法**\n- PE历史分位数\n- PEG估值法\n- DCF现金流折现\n\n**仓位管理**\n- 核心仓位60%(优质蓝筹)\n- 卫星仓位30%(成长股)\n- 现金储备10%", + images: [], + tags: ["投资策略", "经验分享"], + category: "experience", + likes_count: 289, + comments_count: 76, + views_count: 4567, + created_at: "2024-12-12T13:00:00Z", + updated_at: "2024-12-12T13:00:00Z", + is_pinned: true, + status: "active", + }, + { + id: "post_010", + author_id: "user_1", + author_name: "价值投资者", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + title: "军工板块:国防建设加速下的投资机会", + content: + "在复杂国际形势下,国防建设持续加码:\n\n**行业驱动力**\n- 国防预算稳定增长\n- 装备更新换代周期\n- 军民融合深化\n\n**重点方向**\n1. 航空发动机(航发动力)\n2. 导弹武器(航天电器)\n3. 信息化装备(中航电子)\n4. 新材料(光威复材)\n\n估值方面,板块PE约50倍,中长期看仍有配置价值。", + images: [ + "https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=800", + ], + tags: ["军工", "A股"], + category: "analysis", + likes_count: 98, + comments_count: 31, + views_count: 1678, + created_at: "2024-12-11T09:45:00Z", + updated_at: "2024-12-11T09:45:00Z", + is_pinned: false, + status: "active", + }, +]; + +// 帖子评论 +export const mockPostComments = { + post_001: [ + { + id: "comment_001_1", + post_id: "post_001", + author_id: "user_2", + author_name: "趋势猎手", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2", + content: "分析得很透彻!宁德时代确实是新能源赛道的核心资产,长期看好。", + parent_id: null, + likes_count: 23, + created_at: "2024-12-20T11:30:00Z", + status: "active", + }, + { + id: "comment_001_2", + post_id: "post_001", + author_id: "user_3", + author_name: "量化先锋", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3", + content: + "补充一点:钠离子电池的商业化进度值得关注,可能会打开新的增长空间。", + parent_id: null, + likes_count: 18, + created_at: "2024-12-20T12:15:00Z", + status: "active", + }, + { + id: "comment_001_3", + post_id: "post_001", + author_id: "user_4", + author_name: "股市老兵", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + content: "目前估值还是偏贵,等回调到20倍PE再考虑加仓。", + parent_id: null, + likes_count: 12, + created_at: "2024-12-20T14:00:00Z", + status: "active", + }, + ], + post_003: [ + { + id: "comment_003_1", + post_id: "post_003", + author_id: "user_1", + author_name: "价值投资者", + author_avatar: + "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + content: "英伟达的护城河确实深,但估值也确实贵。我选择定投,分批建仓。", + parent_id: null, + likes_count: 34, + created_at: "2024-12-18T10:30:00Z", + status: "active", + }, + { + id: "comment_003_2", + post_id: "post_003", + author_id: "user_4", + author_name: "股市老兵", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + content: "AMD的MI300确实有竞争力,但CUDA生态不是一朝一夕能追上的。", + parent_id: null, + likes_count: 28, + created_at: "2024-12-18T11:45:00Z", + status: "active", + }, + ], + post_005: [ + { + id: "comment_005_1", + post_id: "post_005", + author_id: "user_4", + author_name: "股市老兵", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4", + content: + "最重要的是理解公司的商业模式和竞争优势。PE只是参考,不同行业估值中枢不同。", + parent_id: null, + likes_count: 45, + created_at: "2024-12-16T12:00:00Z", + status: "active", + }, + { + id: "comment_005_2", + post_id: "post_005", + author_id: "user_1", + author_name: "价值投资者", + author_avatar: + "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1", + content: + "建议多看看公司年报,特别是管理层讨论分析部分。另外ROE是个很重要的指标。", + parent_id: null, + likes_count: 38, + created_at: "2024-12-16T13:30:00Z", + status: "active", + }, + { + id: "comment_005_3", + post_id: "post_005", + author_id: "user_3", + author_name: "量化先锋", + author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3", + content: + "可以用PEG来判断成长股估值是否合理,PEG=PE/盈利增速,小于1通常说明被低估。", + parent_id: null, + likes_count: 32, + created_at: "2024-12-16T15:00:00Z", + status: "active", + }, + ], +}; + +export default { + mockForumUsers, + mockTags, + mockCategories, + mockPosts, + mockPostComments, +}; diff --git a/src/mocks/handlers/forum.js b/src/mocks/handlers/forum.js new file mode 100644 index 00000000..2d8753b6 --- /dev/null +++ b/src/mocks/handlers/forum.js @@ -0,0 +1,311 @@ +/** + * 价值论坛帖子 Mock Handler + * 模拟 Elasticsearch API 响应 + */ + +import { http, HttpResponse, delay } from "msw"; +import { mockPosts, mockPostComments } from "../data/forum"; + +// 内存中的帖子数据(支持增删改查) +let postsData = [...mockPosts]; +let commentsData = { ...mockPostComments }; + +// ES 基础 URL(开发环境直连) +const ES_BASE_URL = "http://222.128.1.157:19200"; + +export const forumHandlers = [ + // ==================== 帖子搜索 ==================== + // POST /forum_posts/_search + http.post(`${ES_BASE_URL}/forum_posts/_search`, async ({ request }) => { + await delay(200); + + const body = await request.json(); + const { from = 0, size = 20, query, sort } = body; + + let filteredPosts = [...postsData]; + + // 处理查询条件 + if (query?.bool?.must) { + query.bool.must.forEach((condition) => { + // 状态过滤 + if (condition.match?.status) { + filteredPosts = filteredPosts.filter( + (p) => p.status === condition.match.status + ); + } + // 分类过滤 + if (condition.term?.category) { + filteredPosts = filteredPosts.filter( + (p) => p.category === condition.term.category + ); + } + // 标签过滤 + if (condition.terms?.tags) { + filteredPosts = filteredPosts.filter((p) => + p.tags.some((tag) => condition.terms.tags.includes(tag)) + ); + } + // 全文搜索 + if (condition.multi_match) { + const keyword = condition.multi_match.query.toLowerCase(); + filteredPosts = filteredPosts.filter( + (p) => + p.title.toLowerCase().includes(keyword) || + p.content.toLowerCase().includes(keyword) || + p.tags.some((tag) => tag.toLowerCase().includes(keyword)) + ); + } + }); + } + + // 处理排序 + if (sort && sort.length > 0) { + filteredPosts.sort((a, b) => { + for (const sortItem of sort) { + const [field, config] = Object.entries(sortItem)[0]; + const order = config.order || "desc"; + + if (field === "is_pinned") { + if (a.is_pinned !== b.is_pinned) { + return order === "desc" + ? (b.is_pinned ? 1 : 0) - (a.is_pinned ? 1 : 0) + : (a.is_pinned ? 1 : 0) - (b.is_pinned ? 1 : 0); + } + } else if (field === "created_at") { + const dateA = new Date(a.created_at).getTime(); + const dateB = new Date(b.created_at).getTime(); + if (dateA !== dateB) { + return order === "desc" ? dateB - dateA : dateA - dateB; + } + } else if (field === "likes_count" || field === "views_count") { + if (a[field] !== b[field]) { + return order === "desc" + ? b[field] - a[field] + : a[field] - b[field]; + } + } + } + return 0; + }); + } + + // 分页 + const total = filteredPosts.length; + const paginatedPosts = filteredPosts.slice(from, from + size); + + return HttpResponse.json({ + hits: { + total: { value: total }, + hits: paginatedPosts.map((post) => ({ + _id: post.id, + _source: post, + })), + }, + }); + }), + + // ==================== 获取单个帖子 ==================== + // GET /forum_posts/_doc/:postId + http.get(`${ES_BASE_URL}/forum_posts/_doc/:postId`, async ({ params }) => { + await delay(100); + + const { postId } = params; + const post = postsData.find((p) => p.id === postId); + + if (!post) { + return HttpResponse.json({ found: false }, { status: 404 }); + } + + return HttpResponse.json({ + _id: post.id, + _source: post, + found: true, + }); + }), + + // ==================== 创建帖子 ==================== + // POST /forum_posts/_doc/:postId + http.post( + `${ES_BASE_URL}/forum_posts/_doc/:postId`, + async ({ params, request }) => { + await delay(300); + + const { postId } = params; + const postData = await request.json(); + + const newPost = { + ...postData, + id: postId, + }; + + postsData.unshift(newPost); + + return HttpResponse.json({ + _id: postId, + result: "created", + }); + } + ), + + // ==================== 更新帖子(增加浏览量等) ==================== + // POST /forum_posts/_update/:postId + http.post( + `${ES_BASE_URL}/forum_posts/_update/:postId`, + async ({ params, request }) => { + await delay(100); + + const { postId } = params; + const body = await request.json(); + const postIndex = postsData.findIndex((p) => p.id === postId); + + if (postIndex === -1) { + return HttpResponse.json({ error: "Post not found" }, { status: 404 }); + } + + // 处理脚本更新(如增加浏览量) + if (body.script?.source) { + if (body.script.source.includes("views_count")) { + postsData[postIndex].views_count += 1; + } else if (body.script.source.includes("likes_count")) { + postsData[postIndex].likes_count += 1; + } else if (body.script.source.includes("comments_count")) { + postsData[postIndex].comments_count += 1; + } + } + + // 处理文档更新 + if (body.doc) { + postsData[postIndex] = { ...postsData[postIndex], ...body.doc }; + } + + return HttpResponse.json({ + _id: postId, + result: "updated", + }); + } + ), + + // ==================== 评论搜索 ==================== + // POST /forum_comments/_search + http.post(`${ES_BASE_URL}/forum_comments/_search`, async ({ request }) => { + await delay(150); + + const body = await request.json(); + const { from = 0, size = 50, query } = body; + + let postId = null; + + // 提取 post_id + if (query?.bool?.must) { + const postIdCondition = query.bool.must.find((c) => c.term?.post_id); + if (postIdCondition) { + postId = postIdCondition.term.post_id; + } + } + + const comments = postId ? commentsData[postId] || [] : []; + const activeComments = comments.filter((c) => c.status === "active"); + const paginatedComments = activeComments.slice(from, from + size); + + return HttpResponse.json({ + hits: { + total: { value: activeComments.length }, + hits: paginatedComments.map((comment) => ({ + _id: comment.id, + _source: comment, + })), + }, + }); + }), + + // ==================== 创建评论 ==================== + // POST /forum_comments/_doc/:commentId + http.post( + `${ES_BASE_URL}/forum_comments/_doc/:commentId`, + async ({ params, request }) => { + await delay(200); + + const { commentId } = params; + const commentData = await request.json(); + + const newComment = { + ...commentData, + id: commentId, + }; + + const postId = commentData.post_id; + if (!commentsData[postId]) { + commentsData[postId] = []; + } + commentsData[postId].push(newComment); + + return HttpResponse.json({ + _id: commentId, + result: "created", + }); + } + ), + + // ==================== 更新评论(点赞等) ==================== + // POST /forum_comments/_update/:commentId + http.post( + `${ES_BASE_URL}/forum_comments/_update/:commentId`, + async ({ params, request }) => { + await delay(100); + + const { commentId } = params; + const body = await request.json(); + + // 在所有帖子的评论中查找 + for (const postId in commentsData) { + const commentIndex = commentsData[postId].findIndex( + (c) => c.id === commentId + ); + if (commentIndex !== -1) { + if (body.script?.source?.includes("likes_count")) { + commentsData[postId][commentIndex].likes_count += 1; + } + break; + } + } + + return HttpResponse.json({ + _id: commentId, + result: "updated", + }); + } + ), + + // ==================== 事件时间轴搜索 ==================== + // POST /forum_events/_search + http.post(`${ES_BASE_URL}/forum_events/_search`, async () => { + await delay(100); + + // 返回空数组(暂无事件数据) + return HttpResponse.json({ + hits: { + total: { value: 0 }, + hits: [], + }, + }); + }), + + // ==================== 索引创建(初始化时调用) ==================== + // PUT /forum_posts, /forum_comments, /forum_events + http.put(`${ES_BASE_URL}/forum_posts`, async () => { + await delay(100); + return HttpResponse.json({ acknowledged: true }); + }), + + http.put(`${ES_BASE_URL}/forum_comments`, async () => { + await delay(100); + return HttpResponse.json({ acknowledged: true }); + }), + + http.put(`${ES_BASE_URL}/forum_events`, async () => { + await delay(100); + return HttpResponse.json({ acknowledged: true }); + }), +]; + +export default forumHandlers;