Compare commits
9 Commits
302acbafe3
...
d926b60f03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d926b60f03 | ||
|
|
68eb2380ad | ||
|
|
de30489076 | ||
|
|
df90fc258b | ||
|
|
e283135ef8 | ||
|
|
6b2d883de8 | ||
|
|
0f7a3c0cc9 | ||
|
|
0adceb94f8 | ||
|
|
c9801014c7 |
@@ -289,53 +289,412 @@ export const mockEventComments = [
|
||||
// ==================== 投资计划与复盘数据 ====================
|
||||
|
||||
export const mockInvestmentPlans = [
|
||||
// ==================== 计划数据(符合计划模板) ====================
|
||||
{
|
||||
id: 301,
|
||||
user_id: 1,
|
||||
type: 'plan',
|
||||
title: '2025年Q1 新能源板块布局计划',
|
||||
content: '计划在Q1分批建仓新能源板块,重点关注宁德时代、比亚迪、隆基绿能三只标的。目标仓位15%,预计收益率20%。\n\n具体策略:\n1. 宁德时代:占比6%,等待回调至160元附近分批买入\n2. 比亚迪:占比6%,当前价位可以开始建仓\n3. 隆基绿能:占比3%,观察光伏行业景气度再决定\n\n风险控制:单只个股止损-8%,板块整体止损-10%',
|
||||
content: `【目标】
|
||||
在Q1末实现新能源板块仓位15%,预计收益率20%,重点捕捉新能源政策利好和销量数据催化。
|
||||
|
||||
【策略】
|
||||
1. 宁德时代:占比6%,等待回调至160元附近分批买入,技术面看好底部放量信号
|
||||
2. 比亚迪:占比6%,当前价位可以开始建仓,采用金字塔式加仓
|
||||
3. 隆基绿能:占比3%,观察光伏行业景气度再决定,等待基本面拐点确认
|
||||
|
||||
【风险控制】
|
||||
- 单只个股止损-8%
|
||||
- 板块整体止损-10%
|
||||
- 遇到系统性风险事件,果断减仓50%
|
||||
- 避免在重大财报日前重仓
|
||||
|
||||
【时间规划】
|
||||
- 1月中旬:完成第一批建仓(5%仓位)
|
||||
- 2月春节后:根据市场情况加仓(5%仓位)
|
||||
- 3月中旬:完成最终布局(5%仓位)
|
||||
- 季度末:复盘调整,决定是否持有到Q2`,
|
||||
target_date: '2025-03-31',
|
||||
status: 'in_progress',
|
||||
created_at: '2025-01-10T10:00:00Z',
|
||||
updated_at: '2025-01-15T14:30:00Z',
|
||||
tags: ['新能源', '布局计划', 'Q1计划']
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '2024年12月投资复盘 - 白酒板块大涨',
|
||||
content: '12月白酒板块表现优异,贵州茅台上涨12%,五粮液上涨8%。\n\n操作回顾:\n1. 11月底在1550元加仓茅台,获利6.5%\n2. 五粮液持仓未动,获利4.2%\n\n经验总结:\n- 消费板块在年底有明显的估值修复行情\n- 龙头白马股在市场震荡时更具韧性\n- 应该更大胆一些,仓位可以再提高2-3个点\n\n下月计划:\n- 继续持有茅台、五粮液,不轻易卖出\n- 关注春节前的消费旺季催化',
|
||||
target_date: '2024-12-31',
|
||||
status: 'completed',
|
||||
created_at: '2025-01-02T09:00:00Z',
|
||||
updated_at: '2025-01-02T09:00:00Z',
|
||||
tags: ['月度复盘', '白酒', '2024年12月']
|
||||
tags: ['新能源', '布局计划', 'Q1计划'],
|
||||
stocks: ['300750.SZ', '002594.SZ', '601012.SH']
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
user_id: 1,
|
||||
type: 'plan',
|
||||
title: 'AI 算力板块波段交易计划',
|
||||
content: '随着ChatGPT-5即将发布,AI算力板块有望迎来新一轮炒作。\n\n标的选择:\n- 寒武纪:AI芯片龙头,弹性最大\n- 中科曙光:服务器厂商,业绩支撑\n- 浪潮信息:算力基础设施\n\n交易策略:\n- 仓位控制在10%以内(高风险高弹性)\n- 采用金字塔式买入,第一笔3%\n- 快进快出,涨幅20%分批止盈\n- 破位及时止损,控制在-5%以内',
|
||||
content: `【目标】
|
||||
捕捉ChatGPT-5发布带来的AI算力板块短期行情,目标收益15-20%,控制最大回撤在8%以内。
|
||||
|
||||
【策略】
|
||||
- 寒武纪:AI芯片龙头,弹性最大,首选标的
|
||||
- 中科曙光:服务器厂商,业绩支撑更扎实
|
||||
- 浪潮信息:算力基础设施,流动性好
|
||||
- 采用金字塔式买入,第一笔3%,后续根据走势加仓
|
||||
- 快进快出,涨幅20%分批止盈
|
||||
|
||||
【风险控制】
|
||||
- 仓位控制在10%以内(高风险高弹性)
|
||||
- 单只个股止损-5%
|
||||
- 破位及时止损,不恋战
|
||||
- 避免追高,只在回调时介入
|
||||
|
||||
【时间规划】
|
||||
- 本周:观察消息面发酵情况,确定进场时机
|
||||
- 发布前1周:逐步建仓
|
||||
- 发布后:根据市场反应决定持有还是止盈
|
||||
- 2月底前:完成此轮操作`,
|
||||
target_date: '2025-02-28',
|
||||
status: 'pending',
|
||||
created_at: '2025-01-14T16:00:00Z',
|
||||
updated_at: '2025-01-14T16:00:00Z',
|
||||
tags: ['AI', '算力', '波段交易']
|
||||
tags: ['AI', '算力', '波段交易'],
|
||||
stocks: ['688256.SH', '603019.SH', '000977.SZ']
|
||||
},
|
||||
{
|
||||
id: 305,
|
||||
user_id: 1,
|
||||
type: 'plan',
|
||||
title: '银行股防守配置计划',
|
||||
content: `【目标】
|
||||
构建15%仓位的银行股防守配置,获取稳定分红收益(股息率5%+),同时等待估值修复带来的资本利得。
|
||||
|
||||
【策略】
|
||||
1. 招商银行:零售银行龙头,ROE持续优秀,配置8%
|
||||
2. 兴业银行:同业业务优势明显,配置4%
|
||||
3. 成都银行:城商行中成长性最好,配置3%
|
||||
选股逻辑:优先选择ROE高、资产质量好、分红稳定的标的
|
||||
|
||||
【风险控制】
|
||||
- 银行股整体波动较小,但需关注宏观经济风险
|
||||
- 如遇利率大幅下行或地产风险暴露,需重新评估持仓
|
||||
- 单只银行股止损-15%(较宽松,适合长线持有)
|
||||
- 定期关注季报中的不良贷款率和拨备覆盖率
|
||||
|
||||
【时间规划】
|
||||
- 春节前:完成建仓
|
||||
- 全年持有:享受分红收益
|
||||
- 年中复盘:根据半年报调整配置比例
|
||||
- 年底:评估是否继续持有到下一年`,
|
||||
target_date: '2025-06-30',
|
||||
status: 'active',
|
||||
created_at: '2025-01-08T11:00:00Z',
|
||||
updated_at: '2025-01-08T11:00:00Z',
|
||||
tags: ['银行', '防守配置', '高股息'],
|
||||
stocks: ['600036.SH', '601166.SH', '601838.SH']
|
||||
},
|
||||
{
|
||||
id: 306,
|
||||
user_id: 1,
|
||||
type: 'plan',
|
||||
title: '医药创新药中长线布局',
|
||||
content: `【目标】
|
||||
布局医药创新药板块,目标3-6个月内获得25%收益,享受创新药产品上市带来的业绩爆发。
|
||||
|
||||
【策略】
|
||||
1. 恒瑞医药:创新药管线最丰富,PD-1放量进行中
|
||||
2. 药明康德:CRO龙头,受益于全球创新药研发外包
|
||||
3. 百济神州:海外收入占比高,泽布替尼持续放量
|
||||
采用分批建仓策略,避免一次性重仓
|
||||
|
||||
【风险控制】
|
||||
- 总仓位不超过12%
|
||||
- 单只个股止损-10%
|
||||
- 关注集采政策风险,如有利空政策出台立即减仓
|
||||
- 关注核心产品的销售数据和临床进展
|
||||
|
||||
【时间规划】
|
||||
- 第1个月:建立6%底仓
|
||||
- 第2-3个月:根据业绩催化加仓至12%
|
||||
- 第4-6个月:达到目标收益后分批止盈
|
||||
- 每月关注:产品获批进展、销售数据、研报观点`,
|
||||
target_date: '2025-06-30',
|
||||
status: 'active',
|
||||
created_at: '2025-01-05T14:00:00Z',
|
||||
updated_at: '2025-01-12T09:30:00Z',
|
||||
tags: ['医药', '创新药', '中长线'],
|
||||
stocks: ['600276.SH', '603259.SH', '688235.SH']
|
||||
},
|
||||
{
|
||||
id: 307,
|
||||
user_id: 1,
|
||||
type: 'plan',
|
||||
title: '消费复苏主题布局计划',
|
||||
content: `【目标】
|
||||
捕捉春节消费旺季和全年消费复苏趋势,目标收益20%,重点布局白酒和免税龙头。
|
||||
|
||||
【策略】
|
||||
1. 贵州茅台:高端白酒龙头,提价预期+渠道优化
|
||||
2. 五粮液:次高端领军,估值修复空间大
|
||||
3. 中国中免:免税龙头,海南自贸港政策利好
|
||||
分散配置,每只占比3-5%
|
||||
|
||||
【风险控制】
|
||||
- 总仓位控制在15%以内
|
||||
- 单只个股止损-8%
|
||||
- 关注消费数据变化,如不及预期及时调整
|
||||
- 警惕宏观经济下行风险对消费的冲击
|
||||
|
||||
【时间规划】
|
||||
- 春节前2周:完成建仓
|
||||
- 春节后:观察销售数据和股价反应
|
||||
- Q1末:根据一季度消费数据决定是否加仓
|
||||
- 全年跟踪:月度社零数据、旅游数据`,
|
||||
target_date: '2025-04-30',
|
||||
status: 'pending',
|
||||
created_at: '2025-01-03T10:30:00Z',
|
||||
updated_at: '2025-01-03T10:30:00Z',
|
||||
tags: ['消费', '白酒', '免税'],
|
||||
stocks: ['600519.SH', '000858.SZ', '601888.SH']
|
||||
},
|
||||
|
||||
// ==================== 复盘数据(符合复盘模板) ====================
|
||||
{
|
||||
id: 302,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '2024年12月投资复盘 - 白酒板块大涨',
|
||||
content: `【操作回顾】
|
||||
1. 11月底在1550元加仓茅台0.5%仓位,持有至今
|
||||
2. 五粮液持仓未动,从11月初一直持有
|
||||
3. 错过了洋河股份的反弹行情
|
||||
4. 月中短线做了一次泸州老窖,小赚2%出局
|
||||
|
||||
【盈亏分析】
|
||||
- 贵州茅台:获利6.5%,贡献账户收益约0.65%
|
||||
- 五粮液:获利4.2%,贡献账户收益约0.42%
|
||||
- 泸州老窖:短线获利2%,贡献约0.06%
|
||||
- 月度总收益:约1.13%
|
||||
- 同期沪深300涨幅:0.8%,跑赢指数0.33%
|
||||
|
||||
【经验总结】
|
||||
- 消费板块在年底有明显的估值修复行情,这个规律可以记住
|
||||
- 龙头白马股在市场震荡时更具韧性,应该坚定持有
|
||||
- 应该更大胆一些,茅台仓位可以再提高2-3个点
|
||||
- 洋河的机会没把握住,主要是对二线白酒信心不足
|
||||
|
||||
【后续调整】
|
||||
- 继续持有茅台、五粮液,不轻易卖出
|
||||
- 关注春节前的消费旺季催化
|
||||
- 如果有回调,考虑加仓茅台至5%总仓位
|
||||
- 下月开始关注春节消费数据`,
|
||||
target_date: '2024-12-31',
|
||||
status: 'completed',
|
||||
created_at: '2025-01-02T09:00:00Z',
|
||||
updated_at: '2025-01-02T09:00:00Z',
|
||||
tags: ['月度复盘', '白酒', '2024年12月'],
|
||||
stocks: ['600519.SH', '000858.SZ', '000568.SZ']
|
||||
},
|
||||
{
|
||||
id: 304,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '2024年全年投资总结 - 收益率25.6%',
|
||||
content: '2024年全年收益率25.6%,跑赢沪深300指数12个百分点。\n\n全年亮点:\n1. 新能源板块贡献最大,年度收益35%\n2. 白酒板块稳健增长,年度收益18%\n3. 半导体板块波动较大,年度收益8%\n\n教训与反思:\n1. 年初追高了一些热门概念股,后续回调损失较大\n2. 止损执行不够坚决,有两次错过最佳止损时机\n3. 仓位管理有待提高,牛市时仓位偏低\n\n2025年目标:\n- 收益率目标:30%\n- 优化仓位管理,提高资金使用效率\n- 严格执行止损纪律\n- 加强行业研究,提前布局',
|
||||
content: `【操作回顾】
|
||||
1. 全年共进行交易52次,其中胜率62%
|
||||
2. 主要盈利来源:新能源(+35%)、白酒(+18%)
|
||||
3. 主要亏损来源:年初追高的概念股(-8%)
|
||||
4. 最成功操作:5月底抄底宁德时代,持有3个月获利45%
|
||||
5. 最失败操作:3月追高机器人概念,亏损12%割肉
|
||||
|
||||
【盈亏分析】
|
||||
- 全年总收益率:25.6%
|
||||
- 沪深300涨幅:13.6%
|
||||
- 超额收益:12个百分点
|
||||
- 最大回撤:-8.5%(3月份)
|
||||
- 夏普比率:约1.8
|
||||
- 各板块贡献:
|
||||
- 新能源:+12.6%
|
||||
- 白酒:+7.2%
|
||||
- 半导体:+3.2%
|
||||
- 其他:+2.6%
|
||||
|
||||
【经验总结】
|
||||
1. 年初追高热门概念股是最大教训,后续回调损失较大
|
||||
2. 止损执行不够坚决,有两次错过最佳止损时机
|
||||
3. 仓位管理有待提高,牛市时仓位偏低(最高才70%)
|
||||
4. 成功的操作都是逆向买入+耐心持有
|
||||
5. 频繁交易并没有带来更好收益
|
||||
|
||||
【后续调整】
|
||||
2025年目标:
|
||||
- 收益率目标:30%
|
||||
- 优化仓位管理,提高资金使用效率至80%+
|
||||
- 严格执行止损纪律,设置自动止损提醒
|
||||
- 加强行业研究,提前布局而非追高
|
||||
- 减少交易频率,提高单次交易质量`,
|
||||
target_date: '2024-12-31',
|
||||
status: 'completed',
|
||||
created_at: '2025-01-01T10:00:00Z',
|
||||
updated_at: '2025-01-01T10:00:00Z',
|
||||
tags: ['年度复盘', '2024年', '总结']
|
||||
tags: ['年度复盘', '2024年', '总结'],
|
||||
stocks: []
|
||||
},
|
||||
{
|
||||
id: 308,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '宁德时代波段操作复盘',
|
||||
content: `【操作回顾】
|
||||
- 5月25日:在160元附近建仓3%,理由是估值回到历史低位+储能业务放量预期
|
||||
- 6月10日:加仓2%,价格172元,技术面突破关键阻力
|
||||
- 7月20日:再加仓2%,价格195元,财报预告超预期
|
||||
- 8月15日:开始分批止盈,卖出3%仓位,均价235元
|
||||
- 8月28日:清仓剩余4%仓位,均价228元
|
||||
|
||||
【盈亏分析】
|
||||
- 第一笔:160元买入,平均卖出231.5元,收益率44.7%
|
||||
- 第二笔:172元买入,平均卖出231.5元,收益率34.6%
|
||||
- 第三笔:195元买入,平均卖出231.5元,收益率18.7%
|
||||
- 加权平均收益率:约35%
|
||||
- 持仓时间:约3个月
|
||||
- 年化收益率:约140%
|
||||
|
||||
【经验总结】
|
||||
1. 在估值底部+催化剂出现时建仓是正确的选择
|
||||
2. 金字塔式加仓策略有效控制了成本
|
||||
3. 分批止盈策略让我吃到了大部分涨幅
|
||||
4. 但最后一笔加仓(195元)价格偏高,拉低了整体收益
|
||||
5. 应该在涨幅达到30%时就开始止盈,而非等到40%+
|
||||
|
||||
【后续调整】
|
||||
- 下次操作宁德时代,设置150-170元为合理买入区间
|
||||
- 涨幅达到25%开始分批止盈
|
||||
- 储能业务是长期逻辑,可以保留2%底仓长期持有
|
||||
- 关注Q4业绩和2025年指引`,
|
||||
target_date: '2024-08-31',
|
||||
status: 'completed',
|
||||
created_at: '2024-09-01T10:00:00Z',
|
||||
updated_at: '2024-09-01T10:00:00Z',
|
||||
tags: ['个股复盘', '宁德时代', '波段'],
|
||||
stocks: ['300750.SZ']
|
||||
},
|
||||
{
|
||||
id: 309,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '11月第三周交易复盘',
|
||||
content: `【操作回顾】
|
||||
周一:观望,未操作
|
||||
周二:买入比亚迪2%仓位,价格248元
|
||||
周三:加仓比亚迪1%,价格252元;卖出中芯国际1%仓位
|
||||
周四:买入恒瑞医药1.5%仓位,价格42元
|
||||
周五:观望,持仓未动
|
||||
|
||||
【盈亏分析】
|
||||
- 本周账户收益:+0.8%
|
||||
- 比亚迪:浮盈1.2%
|
||||
- 恒瑞医药:浮亏0.5%
|
||||
- 中芯国际:卖出盈利3%
|
||||
- 同期沪深300:+0.5%
|
||||
- 超额收益:+0.3%
|
||||
|
||||
【经验总结】
|
||||
1. 比亚迪买入时机较好,趁回调建仓
|
||||
2. 恒瑞医药买得稍早,本周没有继续下跌但也没涨
|
||||
3. 中芯国际止盈时机把握得不错,避免了后续调整
|
||||
4. 交易频率偏高,手续费成本需要注意
|
||||
|
||||
【后续调整】
|
||||
- 下周继续持有比亚迪和恒瑞,等待催化
|
||||
- 如果恒瑞跌破40元,考虑加仓
|
||||
- 比亚迪如果突破260元,可以继续加仓
|
||||
- 下周计划观察AI板块是否有机会`,
|
||||
target_date: '2024-11-24',
|
||||
status: 'completed',
|
||||
created_at: '2024-11-25T18:00:00Z',
|
||||
updated_at: '2024-11-25T18:00:00Z',
|
||||
tags: ['周度复盘', '11月第三周'],
|
||||
stocks: ['002594.SZ', '600276.SH', '688981.SH']
|
||||
},
|
||||
{
|
||||
id: 310,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '机器人概念追高教训复盘',
|
||||
content: `【操作回顾】
|
||||
- 3月5日:看到机器人概念连续大涨,FOMO心态买入机器人ETF 5%仓位
|
||||
- 3月8日:继续上涨,追加3%仓位
|
||||
- 3月12日:见顶回落,犹豫不决
|
||||
- 3月18日:跌破成本价,仍抱有侥幸心理
|
||||
- 3月25日:止损出局,平均亏损12%
|
||||
|
||||
【盈亏分析】
|
||||
- 买入成本:约1.05元(均价)
|
||||
- 卖出价格:约0.92元
|
||||
- 亏损金额:约8%仓位 × 12% = 0.96%账户净值
|
||||
- 这是本年度最大单笔亏损
|
||||
- 教训成本:约5000元
|
||||
|
||||
【经验总结】
|
||||
1. 追高是最大的错误,概念炒作往往来去匆匆
|
||||
2. FOMO心态害死人,看到别人赚钱就想追
|
||||
3. 止损不坚决,跌破成本价时就应该走
|
||||
4. 对机器人行业基本面了解不够,纯粹是跟风
|
||||
5. 仓位太重,首次买入就5%,完全不符合试仓原则
|
||||
|
||||
【后续调整】
|
||||
- 概念炒作坚决不追高,只在调整时考虑
|
||||
- 任何新建仓位首次买入不超过2%
|
||||
- 设置硬性止损-5%,坚决执行
|
||||
- 不熟悉的领域少碰或只做小仓位
|
||||
- 记住这次教训,下次遇到类似情况要克制`,
|
||||
target_date: '2024-03-31',
|
||||
status: 'completed',
|
||||
created_at: '2024-04-01T09:00:00Z',
|
||||
updated_at: '2024-04-01T09:00:00Z',
|
||||
tags: ['教训复盘', '追高', '机器人'],
|
||||
stocks: []
|
||||
},
|
||||
{
|
||||
id: 311,
|
||||
user_id: 1,
|
||||
type: 'review',
|
||||
title: '半导体板块Q3操作复盘',
|
||||
content: `【操作回顾】
|
||||
7月份:
|
||||
- 买入中芯国际3%仓位,价格45元
|
||||
- 买入北方华创2%仓位,价格180元
|
||||
|
||||
8月份:
|
||||
- 中芯国际加仓1%,价格48元
|
||||
- 北方华创持仓不动
|
||||
|
||||
9月份:
|
||||
- 中芯国际在55元分批止盈2%
|
||||
- 北方华创在195元全部止盈
|
||||
- 保留中芯国际2%底仓
|
||||
|
||||
【盈亏分析】
|
||||
- 中芯国际:
|
||||
- 已止盈部分:收益率约20%
|
||||
- 剩余持仓:浮盈约15%
|
||||
- 北方华创:
|
||||
- 全部止盈,收益率约8%
|
||||
- Q3半导体板块总收益:约+3.2%账户净值
|
||||
- 板块贡献排名:第三(仅次于新能源和白酒)
|
||||
|
||||
【经验总结】
|
||||
1. 半导体板块波动大,不适合重仓长持
|
||||
2. 北方华创止盈过早,后来又涨了10%
|
||||
3. 中芯国际的分批止盈策略比较成功
|
||||
4. 应该更多关注设备和材料,而非制造环节
|
||||
5. 华为产业链相关标的值得持续关注
|
||||
|
||||
【后续调整】
|
||||
- Q4继续持有中芯国际底仓
|
||||
- 关注北方华创回调机会
|
||||
- 新增关注标的:长电科技、华虹半导体
|
||||
- 仓位目标:半导体板块不超过10%`,
|
||||
target_date: '2024-09-30',
|
||||
status: 'completed',
|
||||
created_at: '2024-10-08T10:00:00Z',
|
||||
updated_at: '2024-10-08T10:00:00Z',
|
||||
tags: ['季度复盘', '半导体', 'Q3'],
|
||||
stocks: ['688981.SH', '002371.SZ']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -131,9 +131,6 @@ export interface PlanningContextValue {
|
||||
};
|
||||
|
||||
// 颜色主题变量(基于当前主题模式)
|
||||
/** 背景色 */
|
||||
bgColor: string;
|
||||
|
||||
/** 边框颜色 */
|
||||
borderColor: string;
|
||||
|
||||
@@ -146,13 +143,10 @@ export interface PlanningContextValue {
|
||||
/** 卡片背景色 */
|
||||
cardBg: string;
|
||||
|
||||
// 导航方法(可选,用于空状态引导)
|
||||
// 导航方法(用于空状态引导)
|
||||
/** 切换视图模式 */
|
||||
setViewMode?: (mode: 'calendar' | 'list') => void;
|
||||
setViewMode: (mode: 'calendar' | 'list') => void;
|
||||
|
||||
/** 切换列表 Tab */
|
||||
setListTab?: (tab: number) => void;
|
||||
|
||||
/** 关闭弹窗 */
|
||||
closeModal?: () => void;
|
||||
setListTab: (tab: number) => void;
|
||||
}
|
||||
|
||||
@@ -273,18 +273,18 @@ export default function CenterDashboard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box bg={sectionBg} minH="100vh">
|
||||
<Box px={{ base: 4, md: 8 }} py={6} maxW="1400px" mx="auto">
|
||||
<Box bg={sectionBg} minH="100vh" overflowX="hidden">
|
||||
<Box px={{ base: 3, md: 8 }} py={{ base: 4, md: 6 }} maxW="1400px" mx="auto">
|
||||
{/* 主要内容区域 */}
|
||||
<Grid templateColumns={{ base: '1fr', md: '1fr 1fr', lg: 'repeat(3, 1fr)' }} gap={6} mb={8}>
|
||||
<Grid templateColumns={{ base: '1fr', lg: 'repeat(3, 1fr)' }} gap={6} mb={8}>
|
||||
{/* 左列:自选股票 */}
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
|
||||
<CardHeader pb={4}>
|
||||
<VStack spacing={6} align="stretch" minW={0}>
|
||||
<Card bg={cardBg} shadow="md" maxW="100%" height={{ base: 'auto', md: '600px' }} minH={{ base: '300px', md: '600px' }} display="flex" flexDirection="column">
|
||||
<CardHeader pb={{ base: 2, md: 4 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack>
|
||||
<Icon as={FiBarChart2} color="blue.500" boxSize={5} />
|
||||
<Heading size="md">自选股票</Heading>
|
||||
<Icon as={FiBarChart2} color="blue.500" boxSize={{ base: 4, md: 5 }} />
|
||||
<Heading size={{ base: 'sm', md: 'md' }}>自选股票</Heading>
|
||||
<Badge colorScheme="blue" variant="subtle">
|
||||
{watchlist.length}
|
||||
</Badge>
|
||||
@@ -388,14 +388,14 @@ export default function CenterDashboard() {
|
||||
</VStack>
|
||||
|
||||
{/* 中列:关注事件 */}
|
||||
<VStack spacing={6} align="stretch">
|
||||
<VStack spacing={6} align="stretch" minW={0}>
|
||||
{/* 关注事件 */}
|
||||
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
|
||||
<CardHeader pb={4}>
|
||||
<Card bg={cardBg} shadow="md" maxW="100%" height={{ base: 'auto', md: '600px' }} minH={{ base: '300px', md: '600px' }} display="flex" flexDirection="column">
|
||||
<CardHeader pb={{ base: 2, md: 4 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack>
|
||||
<Icon as={FiStar} color="yellow.500" boxSize={5} />
|
||||
<Heading size="md">关注事件</Heading>
|
||||
<Icon as={FiStar} color="yellow.500" boxSize={{ base: 4, md: 5 }} />
|
||||
<Heading size={{ base: 'sm', md: 'md' }}>关注事件</Heading>
|
||||
<Badge colorScheme="yellow" variant="subtle">
|
||||
{followingEvents.length}
|
||||
</Badge>
|
||||
@@ -525,14 +525,14 @@ export default function CenterDashboard() {
|
||||
</VStack>
|
||||
|
||||
{/* 右列:我的评论 */}
|
||||
<VStack spacing={6} align="stretch">
|
||||
<VStack spacing={6} align="stretch" minW={0}>
|
||||
{/* 我的评论 */}
|
||||
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
|
||||
<CardHeader pb={4}>
|
||||
<Card bg={cardBg} shadow="md" maxW="100%" height={{ base: 'auto', md: '600px' }} minH={{ base: '300px', md: '600px' }} display="flex" flexDirection="column">
|
||||
<CardHeader pb={{ base: 2, md: 4 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack>
|
||||
<Icon as={FiMessageSquare} color="purple.500" boxSize={5} />
|
||||
<Heading size="md">我的评论</Heading>
|
||||
<Icon as={FiMessageSquare} color="purple.500" boxSize={{ base: 4, md: 5 }} />
|
||||
<Heading size={{ base: 'sm', md: 'md' }}>我的评论</Heading>
|
||||
<Badge colorScheme="purple" variant="subtle">
|
||||
{eventComments.length}
|
||||
</Badge>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 使用 FullCalendar 展示投资计划、复盘等事件
|
||||
*/
|
||||
|
||||
import React, { useState, lazy, Suspense } from 'react';
|
||||
import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Modal,
|
||||
@@ -12,15 +12,9 @@ import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
VStack,
|
||||
Text,
|
||||
Spinner,
|
||||
Center,
|
||||
Icon,
|
||||
Link,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiCalendar } from 'react-icons/fi';
|
||||
import FullCalendar from '@fullcalendar/react';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
@@ -30,7 +24,7 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
import { usePlanningData } from './PlanningContext';
|
||||
import { EventDetailCard } from './EventDetailCard';
|
||||
import { EventDetailModal } from './EventDetailModal';
|
||||
import type { InvestmentEvent } from '@/types';
|
||||
|
||||
// 懒加载投资日历组件
|
||||
@@ -60,184 +54,119 @@ interface CalendarEvent {
|
||||
export const CalendarPanel: React.FC = () => {
|
||||
const {
|
||||
allEvents,
|
||||
loading,
|
||||
borderColor,
|
||||
secondaryText,
|
||||
setViewMode,
|
||||
setListTab,
|
||||
} = usePlanningData();
|
||||
|
||||
// 详情弹窗
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
// 投资日历弹窗
|
||||
// 弹窗状态(统一使用 useState)
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
|
||||
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
|
||||
|
||||
// 转换数据为 FullCalendar 格式
|
||||
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({
|
||||
...event,
|
||||
id: `${event.source || 'user'}-${event.id}`,
|
||||
title: event.title,
|
||||
start: event.event_date,
|
||||
date: event.event_date,
|
||||
backgroundColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
|
||||
borderColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
|
||||
extendedProps: {
|
||||
// 转换数据为 FullCalendar 格式(使用 useMemo 缓存)
|
||||
const calendarEvents: CalendarEvent[] = useMemo(() =>
|
||||
allEvents.map(event => ({
|
||||
...event,
|
||||
isSystem: event.source === 'future',
|
||||
}
|
||||
}));
|
||||
id: `${event.source || 'user'}-${event.id}`,
|
||||
title: event.title,
|
||||
start: event.event_date,
|
||||
date: event.event_date,
|
||||
backgroundColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
|
||||
borderColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
|
||||
extendedProps: {
|
||||
...event,
|
||||
isSystem: event.source === 'future',
|
||||
}
|
||||
})), [allEvents]);
|
||||
|
||||
// 处理日期点击
|
||||
const handleDateClick = (info: DateClickArg): void => {
|
||||
const clickedDate = dayjs(info.date);
|
||||
// 抽取公共的打开事件详情函数
|
||||
const openEventDetail = useCallback((date: Date | null): void => {
|
||||
if (!date) return;
|
||||
const clickedDate = dayjs(date);
|
||||
setSelectedDate(clickedDate);
|
||||
|
||||
const dayEvents = allEvents.filter(event =>
|
||||
dayjs(event.event_date).isSame(clickedDate, 'day')
|
||||
);
|
||||
setSelectedDateEvents(dayEvents);
|
||||
onOpen();
|
||||
};
|
||||
setIsDetailModalOpen(true);
|
||||
}, [allEvents]);
|
||||
|
||||
// 处理日期点击
|
||||
const handleDateClick = useCallback((info: DateClickArg): void => {
|
||||
openEventDetail(info.date);
|
||||
}, [openEventDetail]);
|
||||
|
||||
// 处理事件点击
|
||||
const handleEventClick = (info: EventClickArg): void => {
|
||||
const event = info.event;
|
||||
const clickedDate = dayjs(event.start);
|
||||
setSelectedDate(clickedDate);
|
||||
|
||||
const dayEvents = allEvents.filter(ev =>
|
||||
dayjs(ev.event_date).isSame(clickedDate, 'day')
|
||||
);
|
||||
setSelectedDateEvents(dayEvents);
|
||||
onOpen();
|
||||
};
|
||||
const handleEventClick = useCallback((info: EventClickArg): void => {
|
||||
openEventDetail(info.event.start);
|
||||
}, [openEventDetail]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{loading ? (
|
||||
<Center h="560px">
|
||||
<Spinner size="xl" color="purple.500" />
|
||||
</Center>
|
||||
) : (
|
||||
<Box height={{ base: '500px', md: '600px' }}>
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="zh-cn"
|
||||
headerToolbar={{
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: ''
|
||||
}}
|
||||
events={calendarEvents}
|
||||
dateClick={handleDateClick}
|
||||
eventClick={handleEventClick}
|
||||
height="100%"
|
||||
dayMaxEvents={3}
|
||||
moreLinkText="更多"
|
||||
buttonText={{
|
||||
today: '今天',
|
||||
month: '月',
|
||||
week: '周'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box height={{ base: '380px', md: '560px' }}>
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="zh-cn"
|
||||
headerToolbar={{
|
||||
left: 'prev,next',
|
||||
center: 'today',
|
||||
right: ''
|
||||
}}
|
||||
events={calendarEvents}
|
||||
dateClick={handleDateClick}
|
||||
eventClick={handleEventClick}
|
||||
height="100%"
|
||||
dayMaxEvents={1}
|
||||
moreLinkText="+更多"
|
||||
buttonText={{
|
||||
today: '今天',
|
||||
month: '月',
|
||||
week: '周'
|
||||
}}
|
||||
titleFormat={{ year: 'numeric', month: 'long' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 查看事件详情 Modal */}
|
||||
{isOpen && (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{selectedDateEvents.length === 0 ? (
|
||||
<Center py={8}>
|
||||
<VStack spacing={3}>
|
||||
<Icon as={FiCalendar} boxSize={10} color="gray.300" />
|
||||
<Text color={secondaryText}>当天暂无事件</Text>
|
||||
<Text fontSize="sm" color={secondaryText}>
|
||||
可在
|
||||
<Link
|
||||
color="purple.500"
|
||||
fontWeight="medium"
|
||||
mx={1}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setViewMode?.('list');
|
||||
setListTab?.(0);
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
计划
|
||||
</Link>
|
||||
或
|
||||
<Link
|
||||
color="green.500"
|
||||
fontWeight="medium"
|
||||
mx={1}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setViewMode?.('list');
|
||||
setListTab?.(1);
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
复盘
|
||||
</Link>
|
||||
添加,或关注
|
||||
<Link
|
||||
color="blue.500"
|
||||
fontWeight="medium"
|
||||
mx={1}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setIsInvestmentCalendarOpen(true);
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
投资日历
|
||||
</Link>
|
||||
中的未来事件
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<VStack align="stretch" spacing={4}>
|
||||
{selectedDateEvents.map((event, idx) => (
|
||||
<EventDetailCard
|
||||
key={idx}
|
||||
event={event}
|
||||
borderColor={borderColor}
|
||||
secondaryText={secondaryText}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
<EventDetailModal
|
||||
isOpen={isDetailModalOpen}
|
||||
onClose={() => setIsDetailModalOpen(false)}
|
||||
selectedDate={selectedDate}
|
||||
events={selectedDateEvents}
|
||||
borderColor={borderColor}
|
||||
secondaryText={secondaryText}
|
||||
onNavigateToPlan={() => {
|
||||
setViewMode('list');
|
||||
setListTab(0);
|
||||
}}
|
||||
onNavigateToReview={() => {
|
||||
setViewMode('list');
|
||||
setListTab(1);
|
||||
}}
|
||||
onOpenInvestmentCalendar={() => {
|
||||
setIsInvestmentCalendarOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 投资日历 Modal */}
|
||||
{isInvestmentCalendarOpen && (
|
||||
<Modal
|
||||
isOpen={isInvestmentCalendarOpen}
|
||||
onClose={() => setIsInvestmentCalendarOpen(false)}
|
||||
size="6xl"
|
||||
size={{ base: 'full', md: '6xl' }}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="1200px">
|
||||
<ModalHeader>投资日历</ModalHeader>
|
||||
<ModalContent maxW={{ base: '100%', md: '1200px' }} mx={{ base: 0, md: 4 }}>
|
||||
<ModalHeader fontSize={{ base: 'md', md: 'lg' }} py={{ base: 3, md: 4 }}>投资日历</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
<Suspense fallback={<Center py={8}><Spinner size="xl" color="blue.500" /></Center>}>
|
||||
<Suspense fallback={<Center py={{ base: 6, md: 8 }}><Spinner size={{ base: 'lg', md: 'xl' }} color="blue.500" /></Center>}>
|
||||
<InvestmentCalendar />
|
||||
</Suspense>
|
||||
</ModalBody>
|
||||
|
||||
@@ -84,15 +84,15 @@ export const EventDetailCard: React.FC<EventDetailCardProps> = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
p={{ base: 3, md: 4 }}
|
||||
borderRadius="md"
|
||||
border="1px"
|
||||
borderColor={borderColor}
|
||||
>
|
||||
{/* 标题和标签 */}
|
||||
<Flex justify="space-between" align="start" mb={2}>
|
||||
<HStack flexWrap="wrap" flex={1}>
|
||||
<Text fontWeight="bold" fontSize="lg">
|
||||
<Flex justify="space-between" align="start" mb={{ base: 1, md: 2 }} gap={{ base: 1, md: 2 }}>
|
||||
<HStack flexWrap="wrap" flex={1} spacing={{ base: 1, md: 2 }} gap={1}>
|
||||
<Text fontWeight="bold" fontSize={{ base: 'md', md: 'lg' }}>
|
||||
{event.title}
|
||||
</Text>
|
||||
{getEventBadge()}
|
||||
@@ -101,10 +101,10 @@ export const EventDetailCard: React.FC<EventDetailCardProps> = ({
|
||||
|
||||
{/* 描述内容 - 支持展开/收起 */}
|
||||
{event.description && (
|
||||
<Box mb={2}>
|
||||
<Box mb={{ base: 1, md: 2 }}>
|
||||
<Text
|
||||
ref={descriptionRef}
|
||||
fontSize="sm"
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
color={secondaryText}
|
||||
noOfLines={isExpanded ? undefined : MAX_LINES}
|
||||
whiteSpace="pre-wrap"
|
||||
@@ -128,8 +128,8 @@ export const EventDetailCard: React.FC<EventDetailCardProps> = ({
|
||||
|
||||
{/* 相关股票 */}
|
||||
{event.stocks && event.stocks.length > 0 && (
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
<Text fontSize="sm" color={secondaryText}>相关股票:</Text>
|
||||
<HStack spacing={{ base: 1, md: 2 }} flexWrap="wrap" gap={1}>
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} color={secondaryText}>相关股票:</Text>
|
||||
{event.stocks.map((stock, i) => (
|
||||
<Tag key={i} size="sm" colorScheme="blue" mb={1}>
|
||||
<TagLeftIcon as={FiTrendingUp} />
|
||||
|
||||
98
src/views/Dashboard/components/EventDetailModal.tsx
Normal file
98
src/views/Dashboard/components/EventDetailModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* EventDetailModal - 事件详情弹窗组件
|
||||
* 用于展示某一天的所有投资事件
|
||||
* 使用 Ant Design 实现
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal, Space } from 'antd';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { EventDetailCard } from './EventDetailCard';
|
||||
import { EventEmptyState } from './EventEmptyState';
|
||||
import type { InvestmentEvent } from '@/types';
|
||||
|
||||
/**
|
||||
* EventDetailModal Props
|
||||
*/
|
||||
export interface EventDetailModalProps {
|
||||
/** 是否打开 */
|
||||
isOpen: boolean;
|
||||
/** 关闭回调 */
|
||||
onClose: () => void;
|
||||
/** 选中的日期 */
|
||||
selectedDate: Dayjs | null;
|
||||
/** 选中日期的事件列表 */
|
||||
events: InvestmentEvent[];
|
||||
/** 边框颜色 */
|
||||
borderColor?: string;
|
||||
/** 次要文字颜色 */
|
||||
secondaryText?: string;
|
||||
/** 导航到计划列表 */
|
||||
onNavigateToPlan?: () => void;
|
||||
/** 导航到复盘列表 */
|
||||
onNavigateToReview?: () => void;
|
||||
/** 打开投资日历 */
|
||||
onOpenInvestmentCalendar?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* EventDetailModal 组件
|
||||
*/
|
||||
export const EventDetailModal: React.FC<EventDetailModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectedDate,
|
||||
events,
|
||||
borderColor,
|
||||
secondaryText,
|
||||
onNavigateToPlan,
|
||||
onNavigateToReview,
|
||||
onOpenInvestmentCalendar,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
title={`${selectedDate?.format('YYYY年MM月DD日') || ''} 的事件`}
|
||||
footer={null}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
keyboard={true}
|
||||
centered
|
||||
styles={{
|
||||
body: { paddingTop: 16, paddingBottom: 24 },
|
||||
}}
|
||||
>
|
||||
{events.length === 0 ? (
|
||||
<EventEmptyState
|
||||
onNavigateToPlan={() => {
|
||||
onClose();
|
||||
onNavigateToPlan?.();
|
||||
}}
|
||||
onNavigateToReview={() => {
|
||||
onClose();
|
||||
onNavigateToReview?.();
|
||||
}}
|
||||
onOpenInvestmentCalendar={() => {
|
||||
onClose();
|
||||
onOpenInvestmentCalendar?.();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
{events.map((event, idx) => (
|
||||
<EventDetailCard
|
||||
key={idx}
|
||||
event={event}
|
||||
borderColor={borderColor}
|
||||
secondaryText={secondaryText}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetailModal;
|
||||
94
src/views/Dashboard/components/EventEmptyState.tsx
Normal file
94
src/views/Dashboard/components/EventEmptyState.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* EventEmptyState - 事件空状态组件
|
||||
* 用于展示日历无事件时的引导提示
|
||||
* 使用 Ant Design 实现
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography, Space, Empty } from 'antd';
|
||||
import { CalendarOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text, Link } = Typography;
|
||||
|
||||
/**
|
||||
* EventEmptyState Props
|
||||
*/
|
||||
export interface EventEmptyStateProps {
|
||||
/** 空状态提示文字 */
|
||||
message?: string;
|
||||
/** 导航到计划列表 */
|
||||
onNavigateToPlan?: () => void;
|
||||
/** 导航到复盘列表 */
|
||||
onNavigateToReview?: () => void;
|
||||
/** 打开投资日历 */
|
||||
onOpenInvestmentCalendar?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* EventEmptyState 组件
|
||||
*/
|
||||
export const EventEmptyState: React.FC<EventEmptyStateProps> = ({
|
||||
message = '当天暂无事件',
|
||||
onNavigateToPlan,
|
||||
onNavigateToReview,
|
||||
onOpenInvestmentCalendar,
|
||||
}) => {
|
||||
// 是否显示引导链接
|
||||
const showGuide = onNavigateToPlan || onNavigateToReview || onOpenInvestmentCalendar;
|
||||
|
||||
// 渲染描述内容
|
||||
const renderDescription = () => {
|
||||
if (!showGuide) {
|
||||
return <Text type="secondary">{message}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={4} style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary">{message}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
可在
|
||||
{onNavigateToPlan && (
|
||||
<Link
|
||||
style={{ margin: '0 4px', color: '#8B5CF6' }}
|
||||
onClick={onNavigateToPlan}
|
||||
>
|
||||
计划
|
||||
</Link>
|
||||
)}
|
||||
{onNavigateToPlan && onNavigateToReview && '或'}
|
||||
{onNavigateToReview && (
|
||||
<Link
|
||||
style={{ margin: '0 4px', color: '#38A169' }}
|
||||
onClick={onNavigateToReview}
|
||||
>
|
||||
复盘
|
||||
</Link>
|
||||
)}
|
||||
添加
|
||||
{onOpenInvestmentCalendar && (
|
||||
<>
|
||||
,或关注
|
||||
<Link
|
||||
style={{ margin: '0 4px', color: '#3182CE' }}
|
||||
onClick={onOpenInvestmentCalendar}
|
||||
>
|
||||
投资日历
|
||||
</Link>
|
||||
中的未来事件
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Empty
|
||||
image={<CalendarOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />}
|
||||
imageStyle={{ height: 60 }}
|
||||
description={renderDescription()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventEmptyState;
|
||||
@@ -8,7 +8,7 @@
|
||||
* - label: 显示文案
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Badge,
|
||||
@@ -58,6 +58,135 @@ interface StatusInfo {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态信息
|
||||
*/
|
||||
const getStatusInfo = (status?: EventStatus): StatusInfo => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return { icon: FiCheckCircle, color: 'green', text: '已完成' };
|
||||
case 'cancelled':
|
||||
return { icon: FiXCircle, color: 'red', text: '已取消' };
|
||||
default:
|
||||
return { icon: FiAlertCircle, color: 'blue', text: '进行中' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EventCard Props
|
||||
*/
|
||||
interface EventCardProps {
|
||||
item: InvestmentEvent;
|
||||
colorScheme: string;
|
||||
label: string;
|
||||
textColor: string;
|
||||
secondaryText: string;
|
||||
cardBg: string;
|
||||
onEdit: (item: InvestmentEvent) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* EventCard 组件(使用 memo 优化渲染性能)
|
||||
*/
|
||||
const EventCard = memo<EventCardProps>(({
|
||||
item,
|
||||
colorScheme,
|
||||
label,
|
||||
textColor,
|
||||
secondaryText,
|
||||
cardBg,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const statusInfo = getStatusInfo(item.status);
|
||||
|
||||
return (
|
||||
<Card
|
||||
bg={cardBg}
|
||||
shadow="sm"
|
||||
_hover={{ shadow: 'md' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<CardBody p={{ base: 2, md: 3 }}>
|
||||
<VStack align="stretch" spacing={{ base: 2, md: 3 }}>
|
||||
<Flex justify="space-between" align="start">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack spacing={{ base: 1, md: 2 }}>
|
||||
<Icon as={FiFileText} color={`${colorScheme}.500`} boxSize={{ base: 4, md: 5 }} />
|
||||
<Text fontWeight="bold" fontSize={{ base: 'md', md: 'lg' }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={{ base: 1, md: 2 }}>
|
||||
<Icon as={FiCalendar} boxSize={{ base: 2.5, md: 3 }} color={secondaryText} />
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} color={secondaryText}>
|
||||
{dayjs(item.event_date || item.date).format('YYYY年MM月DD日')}
|
||||
</Text>
|
||||
<Badge
|
||||
colorScheme={statusInfo.color}
|
||||
variant="subtle"
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack spacing={{ base: 0, md: 1 }}>
|
||||
<IconButton
|
||||
icon={<FiEdit2 />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onEdit(item)}
|
||||
aria-label={`编辑${label}`}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FiTrash2 />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
onClick={() => onDelete(item.id)}
|
||||
aria-label={`删除${label}`}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{(item.content || item.description) && (
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} color={textColor} noOfLines={3}>
|
||||
{item.content || item.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<HStack spacing={{ base: 1, md: 2 }} flexWrap="wrap" gap={1}>
|
||||
{item.stocks && item.stocks.length > 0 && (
|
||||
<>
|
||||
{item.stocks.map((stock, idx) => (
|
||||
<Tag key={idx} size="sm" colorScheme="blue" variant="subtle">
|
||||
<TagLeftIcon as={FiTrendingUp} />
|
||||
<TagLabel>{stock}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<>
|
||||
{item.tags.map((tag, idx) => (
|
||||
<Tag key={idx} size="sm" colorScheme={colorScheme} variant="subtle">
|
||||
<TagLeftIcon as={FiHash} />
|
||||
<TagLabel>{tag}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
EventCard.displayName = 'EventCard';
|
||||
|
||||
/**
|
||||
* EventPanel Props
|
||||
*/
|
||||
@@ -161,105 +290,10 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = (status?: EventStatus): StatusInfo => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return { icon: FiCheckCircle, color: 'green', text: '已完成' };
|
||||
case 'cancelled':
|
||||
return { icon: FiXCircle, color: 'red', text: '已取消' };
|
||||
default:
|
||||
return { icon: FiAlertCircle, color: 'blue', text: '进行中' };
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染单个卡片
|
||||
const renderCard = (item: InvestmentEvent): React.ReactElement => {
|
||||
const statusInfo = getStatusInfo(item.status);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
bg={cardBg}
|
||||
shadow="sm"
|
||||
_hover={{ shadow: 'md' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<CardBody>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<Flex justify="space-between" align="start">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack>
|
||||
<Icon as={FiFileText} color={`${colorScheme}.500`} />
|
||||
<Text fontWeight="bold" fontSize="lg">
|
||||
{item.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FiCalendar} boxSize={3} color={secondaryText} />
|
||||
<Text fontSize="sm" color={secondaryText}>
|
||||
{dayjs(item.event_date || item.date).format('YYYY年MM月DD日')}
|
||||
</Text>
|
||||
<Badge
|
||||
colorScheme={statusInfo.color}
|
||||
variant="subtle"
|
||||
>
|
||||
{statusInfo.text}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<FiEdit2 />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleOpenModal(item)}
|
||||
aria-label={`编辑${label}`}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FiTrash2 />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
aria-label={`删除${label}`}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{(item.content || item.description) && (
|
||||
<Text fontSize="sm" color={textColor} noOfLines={3}>
|
||||
{item.content || item.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{item.stocks && item.stocks.length > 0 && (
|
||||
<>
|
||||
{item.stocks.map((stock, idx) => (
|
||||
<Tag key={idx} size="sm" colorScheme="blue" variant="subtle">
|
||||
<TagLeftIcon as={FiTrendingUp} />
|
||||
<TagLabel>{stock}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<>
|
||||
{item.tags.map((tag, idx) => (
|
||||
<Tag key={idx} size="sm" colorScheme={colorScheme} variant="subtle">
|
||||
<TagLeftIcon as={FiHash} />
|
||||
<TagLabel>{tag}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
// 使用 useCallback 优化回调函数
|
||||
const handleEdit = useCallback((item: InvestmentEvent) => {
|
||||
handleOpenModal(item);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -276,8 +310,20 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
|
||||
{events.map(renderCard)}
|
||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={{ base: 3, md: 4 }}>
|
||||
{events.map(event => (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
item={event}
|
||||
colorScheme={colorScheme}
|
||||
label={label}
|
||||
textColor={textColor}
|
||||
secondaryText={secondaryText}
|
||||
cardBg={cardBg}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
@@ -50,18 +50,87 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
/* 响应式调整 - H5 优化 */
|
||||
@media (max-width: 768px) {
|
||||
.fc-toolbar {
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
.fc-toolbar-chunk {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fc-toolbar-title {
|
||||
margin: 0.5em 0;
|
||||
font-size: 1.1rem !important;
|
||||
margin: 4px 0;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.fc-button-group {
|
||||
margin: 0.2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fc-button {
|
||||
padding: 4px 8px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.fc-today-button {
|
||||
padding: 4px 12px !important;
|
||||
}
|
||||
|
||||
/* 日历头部星期 */
|
||||
.fc-col-header-cell-cushion {
|
||||
font-size: 12px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
/* 日期数字 */
|
||||
.fc-daygrid-day-number {
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* 事件标签 */
|
||||
.fc-event {
|
||||
font-size: 10px !important;
|
||||
padding: 1px 2px;
|
||||
margin: 1px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.fc-event-title {
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 日期单元格 */
|
||||
.fc-daygrid-day {
|
||||
min-height: 60px !important;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-frame {
|
||||
min-height: 60px !important;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-events {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* 更多链接 */
|
||||
.fc-daygrid-more-link {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,6 @@ const InvestmentPlanningCenter: React.FC = () => {
|
||||
openPlanModalTrigger,
|
||||
openReviewModalTrigger,
|
||||
toast,
|
||||
bgColor,
|
||||
borderColor,
|
||||
textColor,
|
||||
secondaryText,
|
||||
@@ -151,23 +150,21 @@ const InvestmentPlanningCenter: React.FC = () => {
|
||||
<Icon as={FiTarget} color="purple.500" boxSize={{ base: 4, md: 5 }} />
|
||||
<Heading size={{ base: 'sm', md: 'md' }}>投资规划中心</Heading>
|
||||
</HStack>
|
||||
{/* 视图切换按钮组 */}
|
||||
<ButtonGroup size={{ base: 'xs', md: 'sm' }} isAttached variant="outline">
|
||||
{/* 视图切换按钮组 - H5隐藏 */}
|
||||
<ButtonGroup size="sm" isAttached variant="outline" display={{ base: 'none', md: 'flex' }}>
|
||||
<Button
|
||||
leftIcon={<Icon as={FiList} boxSize={{ base: 3, md: 4 }} />}
|
||||
leftIcon={<Icon as={FiList} boxSize={4} />}
|
||||
colorScheme={viewMode === 'list' ? 'purple' : 'gray'}
|
||||
variant={viewMode === 'list' ? 'solid' : 'outline'}
|
||||
onClick={() => setViewMode('list')}
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
>
|
||||
列表视图
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<Icon as={FiCalendar} boxSize={{ base: 3, md: 4 }} />}
|
||||
leftIcon={<Icon as={FiCalendar} boxSize={4} />}
|
||||
colorScheme={viewMode === 'calendar' ? 'purple' : 'gray'}
|
||||
variant={viewMode === 'calendar' ? 'solid' : 'outline'}
|
||||
onClick={() => setViewMode('calendar')}
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
>
|
||||
日历视图
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user