欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

实验效果

在这里插入图片描述
在这里插入图片描述
大额度效果
在这里插入图片描述
在这里插入图片描述

国寿险收益速算表系统:基于 Flutter 的金融精算模型与 IRR 收益率动态测绘架构

摘要:保险科技(InsurTech)的底层核心不仅在于渠道的数字化,更在于将晦涩的“精算假设(Actuarial Assumptions)”与“生存表(Mortality Tables)”转化为普通消费者可见、可交互的金融图谱。传统的保险收益演示表通常是厚达几十页的 PDF 静态矩阵,不仅缺乏生命力,更掩盖了内部收益率(Internal Rate of Return, IRR)在时间轴上的收敛真相。本文基于跨平台框架 Flutter,利用领域驱动设计(DDD)重构了增额终身寿险的底层推演引擎。本文从现金流折现模型(DCF)出发,通过手写二分法逼近算子(Bisection Method)解构 IRR 方程,并利用 CustomPaint 高阶光栅化管线实现了“盈亏平衡点(回本点)”的动态追击渲染,完美呈现了金融科技在移动终端的最佳实践。

一、 引言:传统寿险利益演示的黑盒化危机

在购买如“增额终身寿险”、“年金险”等带有强金融属性的人寿保险时,消费者最核心的诉求往往只有两个:

  1. 多久能够回本(现金价值突破累计缴纳保费)?
  2. 长期的真实复利收益到底是多少?

由于保险公司在保单前几年需要扣除高额的初始费用、佣金以及身故风险保费,这导致前期的现金价值极低(退保即面临大幅亏损)。而当缴费期满后,现金价值将开始按照“预定利率”进行指数级的纯复利滚存。
这种非线性的现金流,导致传统的算术平均收益率完全失效。为了揭开这层算术伪装,我们必须引入金融数学界唯一的“照妖镜”——IRR(内部收益率)

通过将这套金融引擎全量植入 Flutter 客户端,我们实现了参数的毫秒级滑动响应,让复利的奇迹与时间的力量,直接在用户的指尖弹射起步。


二、 核心数学与精算模型:现金流折现与复利方程

2.1 寿险现金价值(Cash Value)递推模型

假设用户购买了一份年交保费为 P P P,缴费期为 N N N 年,预定复利为 r r r 的产品。为了模拟真实的保险产品扣费逻辑(前高后低),我们设定了一个随时间衰减的费用率 α ( t ) \alpha(t) α(t)

α ( t ) = 0.5 × N − t + 1 N ( t ≤ N ) \alpha(t) = 0.5 \times \frac{N - t + 1}{N} \quad (t \le N) α(t)=0.5×NNt+1(tN)

则在第 t t t 个保单年度末,当年投入的净资金为 P × ( 1 − α ( t ) ) P \times (1 - \alpha(t)) P×(1α(t))
由此,保单现金价值 C V t CV_t CVt 遵循如下递推关系(这里做了简化,剥离了极其复杂的生命表发生率):

C V t = { ( C V t − 1 + P ( 1 − α ( t ) ) ) × ( 1 + r ) if  t ≤ N C V t − 1 × ( 1 + r ) if  t > N CV_t = \begin{cases} \big(CV_{t-1} + P(1-\alpha(t))\big) \times (1+r) & \text{if } t \le N \\ CV_{t-1} \times (1+r) & \text{if } t > N \end{cases} CVt={(CVt1+P(1α(t)))×(1+r)CVt1×(1+r)if tNif t>N

C V t ≥ ∑ P CV_t \ge \sum P CVtP 时,即达成金融学意义上的“盈亏平衡(Break-even)”。

2.2 IRR 内部收益率方程与高维求解

内部收益率(IRR)被定义为使得项目生命周期内所有现金流的净现值(Net Present Value, NPV)严格等于 0 0 0 的那个魔术折现率。
针对在第 T T T 年退保(Surrender)提取现金价值的投保人,其在第 t t t 极初交纳保费,并在第 T T T 年末取回 C V T CV_T CVT。其 NPV 方程表示为:

NPV ( IRR ) = ∑ t = 0 min ⁡ ( N , T ) − 1 − P ( 1 + IRR ) t + C V T ( 1 + IRR ) T = 0 \text{NPV}(\text{IRR}) = \sum_{t=0}^{\min(N, T)-1} \frac{-P}{(1+\text{IRR})^t} + \frac{CV_T}{(1+\text{IRR})^T} = 0 NPV(IRR)=t=0min(N,T)1(1+IRR)tP+(1+IRR)TCVT=0

上述方程是一个高次多项式,无法通过常规代数求出解析解。在本文工程中,我们采用了计算几何学中最暴力的“二分逼近法(Bisection Method)”在 CPU 中对其进行 100 100 100 次极限强行逼近求解。


三、 系统领域驱动架构 (DDD) 与微观演算图谱

3.1 精算聚合根 UML 类图抽象

我们将精算过程严格地封闭在了 ActuarialEngine 中,任何 UI 层的滑动都不会直接污染底层计算公式,而是重新生成一份只读的(Immutable)矩阵账本。

instantiates

holds state

invokes

delegates rendering

many

PolicyYearData

+int policyYear

+double cumulativePremium

+double cashValue

+double currentIRR

ActuarialEngine

+generatePolicyMatrix(...) : List<PolicyYearData>

-_calculateIRR(...) : double

ActuarialDashboard

-double _annualPremium

-int _paymentTerm

-double _guaranteedRate

-List<PolicyYearData> _matrix

-int _breakevenYear

+void _recalculateMatrix()

ActuarialCurvePainter

+List<PolicyYearData> matrix

+int breakevenYear

+void paint(Canvas canvas, Size size)

3.2 IRR 二分法解算器的极限收敛瀑布流

下方的 Flowchart 展示了在我们的 Dart 引擎中,IRR 是如何从一片未知的混沌区间 [ − 99 % , + 100 % ] [-99\%, +100\%] [99%,+100%] 中通过疯狂试探,最终锁定那个让 NPV 归零的“绝对真理数值”的。

大于0 折现压制力不够

小于等于0 折现力过猛

传入该年保费数组与年末退保现价

设定探索边界 low=-0.99 high=1.00

开启100次极限迭代 i=0 i<100 i++

假定当前折现率 IRR = low+high/2

推演折现 遍历缴纳的每一笔保费求复利折损

推演折现 加上退保时的现金价值贴现

加和得到总体净现值 NPV

判断NPV的符号

拔高下限 low=IRR

压低上限 high=IRR

抵达100次界限?

浮点精度锁定 抛出最终IRR结果


四、 核心代码解剖学:金融级客户端的底层撕裂

要让枯燥的数字在手机上起舞,不仅需要极度严谨的代数学底蕴,更需要榨干 Flutter GPU 渲染管线的每一滴性能。

核心解剖学一:防无限发散的二分法逼近算子

在实际处理中,由于某些特殊产品的设置,其 NPV 曲线可能极其陡峭。常规的牛顿-拉夫逊法(Newton-Raphson)在求导时极易遇到拐点从而导致抛出 NaN。而基于二分法的解算器,虽然笨拙,但却是拥有金融绝对稳定性的重型装甲。

  static double _calculateIRR(double annualPremium, int paymentTerm, int surrenderYear, double cashValue) {
    if (surrenderYear == 0 || cashValue <= 0) return 0.0;
    
    double low = -0.99; // 巨额亏损底线,假设近乎归零
    double high = 1.0;  // 100% 暴利上限
    double irr = 0.0;

    // 【高阶算子】固定步长 100 次,在浮点数学中 2^-100 意味着绝对收敛
    for (int i = 0; i < 100; i++) {
      irr = (low + high) / 2;
      double npv = 0.0;
      
      // 【折现流水】期初交费机制(第 t 年初交纳相当于在时间轴的 t 点发生)
      for (int t = 0; t < paymentTerm && t < surrenderYear; t++) {
        npv -= annualPremium / math.pow(1 + irr, t);
      }
      // 退保在期末发生(资金从保险公司回流至客户,符号为正)
      npv += cashValue / math.pow(1 + irr, surrenderYear);

      // 【修正反馈环】利用大数定律纠偏
      if (npv > 0) {
        low = irr;
      } else {
        high = irr;
      }
    }
    return irr;
  }

工程语义分析
这段极其内敛的代码是整套精算系统的引擎心脏。它的绝妙之处在于:摒弃了一切 while(npv.abs() > 0.0001) 这种容易陷入死循环从而引发客户端假死(ANR)的危险判定。通过物理限定执行 100 次 O ( 1 ) O(1) O(1) 级四则运算,这使得每次拖动滑块引发重绘时,底层的 ActuarialEngine 都能在 0.1 0.1 0.1 毫秒内毫无波澜地抛出长达 60 年的极精 IRR 矩阵数组。

核心解剖学二:状态聚合与矩阵重绘闭环

在滑动侧边栏那些充满高级感的刻度尺时,如果引发全局无脑刷新,在复杂的计算量下必然会掉帧。此处展示了如何在 Widget 生命周期中通过严格的单向数据流动保持高贵。

  void _recalculateMatrix() {
    setState(() {
      // 1. 拦截数据,打向底层精算沙盒
      _matrix = ActuarialEngine.generatePolicyMatrix(
        annualPremium: _annualPremium,
        paymentTerm: _paymentTerm,
        guaranteedRate: _guaranteedRate,
        maxYears: _maxYears,
      );

      // 2. O(N) 级别巡航探测回本奇点
      _breakevenYear = _matrix.indexWhere((data) => data.cashValue >= data.cumulativePremium);
      if (_breakevenYear != -1) {
        _breakevenYear += 1; // 数组索引 0 映射至保单年度的 第 1 年
      }
    });
  }

工程语义分析
这是一个经典的数据清洗过滤层。滑动条的微小变更会极其高频地触发 _recalculateMatrix()。在此方法中,我们通过一次底层的重算拿到了全量数据 _matrix,然后利用高阶集合函数 indexWhere 像导弹索敌一样扫描出了那条代表“胜利”的红蓝交汇线(即现金价值超越了保费总和)。这种解耦确保了 UI 层(包括后面的画布图层)永远只需要进行极简的遍历读值。

核心解剖学三:金融级盈亏平衡点 (Break-even Intersection) 着色管线

在一张带有时间坐标的二维图纸上,让机器去刻画那根代表成本的刚性阶梯红线(累计保费)和那根代表时间力量的指数级抛物线(现金价值)。这便是代码的艺术形态。

    // 【成本壁垒】绘制累计保费线 (刚性的阶梯/折线结构)
    final premiumPath = Path();
    premiumPath.moveTo(0, height);
    for (int i = 0; i < premiumPoints.length; i++) {
      if (i == 0) {
        premiumPath.lineTo(premiumPoints[i].dx, premiumPoints[i].dy);
      } else {
        premiumPath.lineTo(premiumPoints[i].dx, premiumPoints[i].dy);
      }
    }
    // 使用纯度极高的告警红,以提醒这就是我们的法定时本金界线
    canvas.drawPath(
      premiumPath, 
      Paint()..color = const Color(0xFFEF4444)..style = PaintingStyle.stroke..strokeWidth = 2
    );

    // 【生命赞歌】绘制现金价值复利面 (带金色光效的面着色器)
    final cvFillPath = Path.from(cvPath);
    cvFillPath.lineTo(width, height);
    cvFillPath.lineTo(0, height);
    cvFillPath.close();

    final fillPaint = Paint()
      // 利用纵向着色器产生一种金沙沉积的下沉重力感
      ..shader = ui.Gradient.linear(
        Offset(0, 0), Offset(0, height), 
        [const Color(0xFFE5C07B).withValues(alpha: 0.3), const Color(0xFFE5C07B).withValues(alpha: 0.0)]
      )
      ..style = PaintingStyle.fill;
    canvas.drawPath(cvFillPath, fillPaint);

工程语义分析
本处展示了何谓真正高级的 Dashboard 设计。代表保费的 premiumPath 被画成了一根细长而冰冷的红色边框线,因为它象征着不可触碰的投入底线;而代表了财富积累的现金价值,则被我们赋予了带有金色渐变的半透明实体面积(通过在原始 cvPath 的基础上追加下方的屏幕外围,再执行 .close() 从而将单薄的线围成了一个金色的城池)。

核心解剖学四:奇点光晕与虚拟气泡引导系统

当回本奇点被找到时,我们必须用一种极其震撼的方式告知用户:“就在这一年,你的保单迎来了跨时代翻盘”。

    // 核心锚点:盈亏平衡点 (Break-even Intersection Point)
    if (breakevenPoint != null) {
      // 1. 绘制交点下方的扩散光晕特效
      canvas.drawCircle(
        breakevenPoint, 
        8, 
        Paint()..color = const Color(0xFFE5C07B).withValues(alpha: 0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4)
      );
      // 2. 绘制刚性的交点实体
      canvas.drawCircle(breakevenPoint, 4, Paint()..color = Colors.white);
      
      // 3. 绘制从天而降的虚线定位坐标
      _drawDashedLine(
        canvas, 
        Offset(breakevenPoint.dx, breakevenPoint.dy), 
        Offset(breakevenPoint.dx, height), 
        Paint()..color = const Color(0xFFE5C07B)..strokeWidth = 1
      );

      // 4. 空投数据气泡框
      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(breakevenPoint.dx - 40, breakevenPoint.dy - 35, 80, 24), 
        const Radius.circular(4)
      );
      canvas.drawRRect(rect, Paint()..color = const Color(0xFFE5C07B));
      // 这里的文字将悬浮在时间线之上
      _drawText(canvas, '回本达成', Offset(breakevenPoint.dx - 24, breakevenPoint.dy - 30), Colors.black, fontWeight: FontWeight.bold);
    }

工程语义分析
这里的 MaskFilter.blur 发挥了极其致命的诱惑力,它在交汇处打出了一层散漫的柔光;紧随其后的虚线坐标和手动挂载的 RRect 气泡窗,则彻底脱离了普通的图表范式。我们没有借助于笨重的交互弹窗插件,而是直接在最高级别的光栅管线内,手写了坐标漂浮算法。当你在左侧拨动滑块,预定利率从 2.5% 被调到 3.0% 时,这个“回本达成”的金色气泡就会在图表上丝滑地逆流左移!这种极致的人机对撞,足以令所有枯燥的金融精算表格黯然失色。


五、 分析表格与业务拓展指标

在一套大型商业保险推介系统的底层,如果还要加入针对高净值人群(High Net Worth Individuals, HNWI)的财富传承分析系统,上述数据往往会引申出以下衍生标量。

系统衍生精算指标 数学计算源泉 财富管理与法务指导意义 数据库与算力要求
IRR 无尽极限值 lim ⁡ t → 100 I R R ( t ) \lim_{t \to 100} IRR(t) limt100IRR(t) 增额终身寿险最后无限逼近于定价复利(如 3.0%),体现抗击长期通胀的基石稳定性。 float_limit_irr_300
退保绝对差损比 ( C V t − ∑ P ) / ∑ P (CV_t - \sum P) / \sum P (CVtP)/P 当在回本期前退保时所承受的惩罚深度。反映出保险对家庭现金流短期流动性的恐怖锁定能力。 double_surrender_penalty
财富杠杆比率 C V t = 60 / ∑ P CV_{t=60} / \sum P CVt=60/P 在生命终末期如果选择传承给后代,此保单所创造的绝对避税避债财富放大器倍数。 float_wealth_leverage

通过上表可以深刻看出,保险不仅仅是前端销售嘴里的一句口号,它是一张极度精密、基于大数定律和跨周期金融套利的罗网。而我们的 Flutter 客户端程序,便是撕开这张罗网,把底牌直接亮给世人的神兵利器。


六、 结论与终极展望

本篇长文以一款名为“国寿险收益速算表系统”的金融科技架构为例,证明了前端开发人员已经拥有了跨越传统界面搭建,直接插手重度计算与运筹帷幄的核武器权限。

从解构最核心的现金流贴现法开始,我们在 Dart 虚拟机里徒手写下了不会被奇点吞噬的 IRR 求解二分法;并通过极其高贵的金融蓝金配色表以及底层的 CustomPaint,将两条原本枯燥干瘪的数据结构集合,幻化为了充满拉扯感与呼吸感的复利深渊。

通过这一系列的重构,我们不仅赋能了保险行业的数字化推演能力,更在技术底层宣告了:任何复杂隐晦的业务逻辑,在极致的工程架构与数学几何面前,终将原形毕露、熠熠生辉!期待未来(如第021篇),我们将利用这样的底层掌控力,向着涉及海量 K 线渲染与期权套利模型的量化交易监控终端发起最猛烈的总攻!

全部源码

import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:ui' as ui;

void main() {
  runApp(const InsurTechApp());
}

class InsurTechApp extends StatelessWidget {
  const InsurTechApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '寿险精算收益速算测绘台',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0D1321), // 金融深空蓝
        fontFamily: 'Roboto',
      ),
      home: const ActuarialDashboard(),
    );
  }
}

// -----------------------------------------------------------------------------
// 精算领域实体 (Actuarial Domain Entities)
// -----------------------------------------------------------------------------

class PolicyYearData {
  final int policyYear;
  final double cumulativePremium;
  final double cashValue;
  final double currentIRR;

  const PolicyYearData({
    required this.policyYear,
    required this.cumulativePremium,
    required this.cashValue,
    required this.currentIRR,
  });
}

class ActuarialEngine {
  /// 生成生命周期内的保单现金价值与收益矩阵
  /// [annualPremium] 年交保费
  /// [paymentTerm] 缴费期 (如 3, 5, 10年)
  /// [guaranteedRate] 预定复利利率 (如 0.03 代表 3.0%)
  /// [maxYears] 推演年限 (通常推演至 80 岁或 100 岁)
  static List<PolicyYearData> generatePolicyMatrix({
    required double annualPremium,
    required int paymentTerm,
    required double guaranteedRate,
    required int maxYears,
  }) {
    List<PolicyYearData> matrix = [];
    double currentCashValue = 0.0;
    double cumulativePremium = 0.0;

    for (int t = 1; t <= maxYears; t++) {
      if (t <= paymentTerm) {
        cumulativePremium += annualPremium;
        
        // 模拟保险公司前期的各项扣费(初始费用、身故风险保费等)
        // 增额终身寿险通常在缴费期满前后实现“现金价值突破累计保费(回本)”
        // 扣费系数随着时间递减
        double expenseRatio = 0.5 * (paymentTerm - t + 1) / paymentTerm;
        double investedPremium = annualPremium * (1 - expenseRatio);
        
        currentCashValue += investedPremium;
        currentCashValue *= (1 + guaranteedRate);
      } else {
        // 缴费期满后,现金价值按照预定利率进行纯复利滚存
        currentCashValue *= (1 + guaranteedRate);
      }

      // 计算本年度退保的内部收益率 (IRR)
      double irr = _calculateIRR(annualPremium, paymentTerm, t, currentCashValue);

      matrix.add(PolicyYearData(
        policyYear: t,
        cumulativePremium: cumulativePremium,
        cashValue: currentCashValue,
        currentIRR: irr,
      ));
    }

    return matrix;
  }

  /// 利用二分逼近法(Bisection Method)求解净现值(NPV)方程为0时的折现率(IRR)
  static double _calculateIRR(double annualPremium, int paymentTerm, int surrenderYear, double cashValue) {
    if (surrenderYear == 0 || cashValue <= 0) return 0.0;
    
    // 设定二分法收敛区间 [-99%, +100%]
    double low = -0.99;
    double high = 1.0;
    double irr = 0.0;

    // 逼近 100 次以获得金融级的浮点精度
    for (int i = 0; i < 100; i++) {
      irr = (low + high) / 2;
      double npv = 0.0;
      
      // 保费支出为现金流出(负数),假设期初交费
      for (int t = 0; t < paymentTerm && t < surrenderYear; t++) {
        npv -= annualPremium / math.pow(1 + irr, t);
      }
      
      // 退保时提取现金价值为现金流入(正数),假设期末退保提取
      npv += cashValue / math.pow(1 + irr, surrenderYear);

      if (npv > 0) {
        // NPV大于0,说明折现率不足,需要提高下限
        low = irr;
      } else {
        // NPV小于等于0,说明折现率过高,需要降低上限
        high = irr;
      }
    }
    
    return irr;
  }
}

// -----------------------------------------------------------------------------
// 核心工作台 (Main Dashboard)
// -----------------------------------------------------------------------------

class ActuarialDashboard extends StatefulWidget {
  const ActuarialDashboard({super.key});

  @override
  State<ActuarialDashboard> createState() => _ActuarialDashboardState();
}

class _ActuarialDashboardState extends State<ActuarialDashboard> {
  // 表单状态因子
  double _annualPremium = 50000.0; // 默认年交 5万元
  int _paymentTerm = 5;            // 默认缴费期 5年
  double _guaranteedRate = 0.030;  // 默认预定复利 3.0%
  final int _maxYears = 60;        // 测算周期 60 年

  // 衍生矩阵数据
  List<PolicyYearData> _matrix = [];
  int _breakevenYear = -1;         // 回本年份

  @override
  void initState() {
    super.initState();
    _recalculateMatrix();
  }

  void _recalculateMatrix() {
    setState(() {
      _matrix = ActuarialEngine.generatePolicyMatrix(
        annualPremium: _annualPremium,
        paymentTerm: _paymentTerm,
        guaranteedRate: _guaranteedRate,
        maxYears: _maxYears,
      );

      // 寻找首个现金价值覆盖总保费的年份
      _breakevenYear = _matrix.indexWhere((data) => data.cashValue >= data.cumulativePremium);
      if (_breakevenYear != -1) {
        _breakevenYear += 1; // 转换为保单年度 (从1开始)
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final isDesktop = MediaQuery.of(context).size.width > 900;

    return Scaffold(
      appBar: AppBar(
        title: const Text('中国寿险精算与复利 IRR 收益测绘终端', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color(0xFFE5C07B), letterSpacing: 1.2)),
        centerTitle: true,
        backgroundColor: const Color(0xFF090D16),
        elevation: 0,
      ),
      body: isDesktop ? _buildDesktopLayout() : _buildMobileLayout(),
    );
  }

  Widget _buildDesktopLayout() {
    return Padding(
      padding: const EdgeInsets.all(24.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(flex: 2, child: _buildControlPanel()),
          const SizedBox(width: 24),
          Expanded(flex: 5, child: _buildDataVisualizer()),
        ],
      ),
    );
  }

  Widget _buildMobileLayout() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _buildControlPanel(),
          const SizedBox(height: 24),
          _buildDataVisualizer(),
        ],
      ),
    );
  }

  // --- 左侧:参数控制器 ---
  Widget _buildControlPanel() {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: const Color(0xFF161F33),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: const Color(0xFFE5C07B).withValues(alpha: 0.2)),
        boxShadow: [
          BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10)),
        ]
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('精算假设配置 (Assumptions)', style: TextStyle(color: Color(0xFFE5C07B), fontSize: 16, fontWeight: FontWeight.bold)),
          const Divider(color: Colors.white10, height: 32),
          
          _buildSliderInput(
            label: '年交保费 (元)',
            value: _annualPremium,
            min: 10000,
            max: 500000,
            divisions: 49,
            format: (v) => '¥${v.toInt()}',
            onChanged: (v) {
              _annualPremium = v;
              _recalculateMatrix();
            },
          ),
          
          const SizedBox(height: 24),
          const Text('缴费年期 (Term)', style: TextStyle(color: Colors.white70, fontSize: 13)),
          const SizedBox(height: 12),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [3, 5, 10].map((term) {
              final isSelected = _paymentTerm == term;
              return ChoiceChip(
                label: Text('$term 年交'),
                selected: isSelected,
                selectedColor: const Color(0xFFE5C07B).withValues(alpha: 0.2),
                labelStyle: TextStyle(color: isSelected ? const Color(0xFFE5C07B) : Colors.grey, fontWeight: FontWeight.bold),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8), side: BorderSide(color: isSelected ? const Color(0xFFE5C07B) : Colors.transparent)),
                onSelected: (selected) {
                  if (selected) {
                    _paymentTerm = term;
                    _recalculateMatrix();
                  }
                },
              );
            }).toList(),
          ),

          const SizedBox(height: 32),
          _buildSliderInput(
            label: '产品预定复利 (Rate)',
            value: _guaranteedRate,
            min: 0.020,
            max: 0.035,
            divisions: 15,
            format: (v) => '${(v * 100).toStringAsFixed(1)}%',
            onChanged: (v) {
              _guaranteedRate = v;
              _recalculateMatrix();
            },
          ),

          const SizedBox(height: 48),
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: const Color(0xFF090D16),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Column(
              children: [
                _buildSummaryRow('总计投入本金', '¥${(_annualPremium * _paymentTerm).toInt()}'),
                const SizedBox(height: 12),
                _buildSummaryRow('保单现价回本期', _breakevenYear > 0 ? '第 $_breakevenYear 年' : '无法回本', highlight: true),
                const SizedBox(height: 12),
                _buildSummaryRow('第 30 年 IRR', '${(_matrix[29].currentIRR * 100).toStringAsFixed(2)}%'),
                const SizedBox(height: 12),
                _buildSummaryRow('第 60 年现价', '¥${(_matrix[59].cashValue / 10000).toStringAsFixed(1)} 万'),
              ],
            ),
          )
        ],
      ),
    );
  }

  Widget _buildSliderInput({required String label, required double value, required double min, required double max, required int divisions, required String Function(double) format, required ValueChanged<double> onChanged}) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(label, style: const TextStyle(color: Colors.white70, fontSize: 13)),
            Text(format(value), style: const TextStyle(color: Color(0xFF00E5FF), fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'monospace')),
          ],
        ),
        SliderTheme(
          data: SliderTheme.of(context).copyWith(
            activeTrackColor: const Color(0xFF00E5FF),
            inactiveTrackColor: Colors.white10,
            thumbColor: const Color(0xFFE5C07B),
            overlayColor: const Color(0xFFE5C07B).withValues(alpha: 0.2),
          ),
          child: Slider(
            value: value,
            min: min,
            max: max,
            divisions: divisions,
            onChanged: onChanged,
          ),
        ),
      ],
    );
  }

  Widget _buildSummaryRow(String label, String value, {bool highlight = false}) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label, style: const TextStyle(color: Colors.grey, fontSize: 13)),
        Text(value, style: TextStyle(color: highlight ? const Color(0xFFE5C07B) : Colors.white, fontSize: 15, fontWeight: FontWeight.bold)),
      ],
    );
  }

  // --- 右侧:高阶图形着色面板 ---
  Widget _buildDataVisualizer() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          padding: const EdgeInsets.all(24),
          decoration: BoxDecoration(
            color: const Color(0xFF161F33),
            borderRadius: BorderRadius.circular(16),
            border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('现金流测绘与盈亏平衡点追踪 (Cash Flow Matrix)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
              const SizedBox(height: 8),
              Row(
                children: [
                  _buildLegendItem(const Color(0xFFEF4444), '累计缴纳保费 (成本界线)'),
                  const SizedBox(width: 24),
                  _buildLegendItem(const Color(0xFFE5C07B), '保单现金价值 (提取界线)'),
                ],
              ),
              const SizedBox(height: 48),
              
              // 核心光栅渲染器
              SizedBox(
                height: 350,
                child: ActuarialCurveChart(matrix: _matrix, breakevenYear: _breakevenYear),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildLegendItem(Color color, String text) {
    return Row(
      children: [
        Container(width: 12, height: 4, color: color),
        const SizedBox(width: 8),
        Text(text, style: const TextStyle(color: Colors.grey, fontSize: 12)),
      ],
    );
  }
}

// -----------------------------------------------------------------------------
// 高频光栅层:精算交会图谱 (Area Curve & Step Line Chart)
// -----------------------------------------------------------------------------

class ActuarialCurveChart extends StatelessWidget {
  final List<PolicyYearData> matrix;
  final int breakevenYear;

  const ActuarialCurveChart({super.key, required this.matrix, required this.breakevenYear});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.infinite,
      painter: _ActuarialCurvePainter(matrix: matrix, breakevenYear: breakevenYear),
    );
  }
}

class _ActuarialCurvePainter extends CustomPainter {
  final List<PolicyYearData> matrix;
  final int breakevenYear;

  _ActuarialCurvePainter({required this.matrix, required this.breakevenYear});

  @override
  void paint(Canvas canvas, Size size) {
    if (matrix.isEmpty) return;

    final width = size.width;
    final height = size.height;

    // 寻找理论Y轴极值 (取最后一年现金价值为天花板)
    final double maxAmount = matrix.last.cashValue * 1.1;
    final double maxYears = matrix.length.toDouble();

    // 1. 绘制极简金融坐标轴底格
    final gridPaint = Paint()
      ..color = Colors.white.withValues(alpha: 0.05)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;

    for (int i = 0; i <= 4; i++) {
      double y = height - (i / 4) * height;
      canvas.drawLine(Offset(0, y), Offset(width, y), gridPaint);
      if (i > 0) {
        _drawText(canvas, '${(maxAmount * i / 4 / 10000).toStringAsFixed(0)} W', Offset(0, y - 16), Colors.white38);
      }
    }

    // X轴标签 (保单年度)
    for (int i = 10; i <= maxYears; i += 10) {
      double x = (i / maxYears) * width;
      _drawText(canvas, '第 $i 年', Offset(x - 16, height + 8), Colors.white38);
    }

    // 2. 坐标转换映射引擎
    final List<Offset> premiumPoints = [];
    final List<Offset> cvPoints = [];
    Offset? breakevenPoint;

    for (int i = 0; i < matrix.length; i++) {
      final data = matrix[i];
      final double cx = ((i + 1) / maxYears) * width;
      
      final double pY = height - (data.cumulativePremium / maxAmount) * height;
      premiumPoints.add(Offset(cx, pY));

      final double cY = height - (data.cashValue / maxAmount) * height;
      cvPoints.add(Offset(cx, cY));

      // 捕获盈亏平衡交汇点坐标
      if (data.policyYear == breakevenYear) {
        breakevenPoint = Offset(cx, cY);
      }
    }

    // 3. 绘制累计保费线 (刚性的阶梯/折线结构)
    final premiumPath = Path();
    premiumPath.moveTo(0, height);
    for (int i = 0; i < premiumPoints.length; i++) {
      if (i == 0) {
        premiumPath.lineTo(premiumPoints[i].dx, premiumPoints[i].dy);
      } else {
        premiumPath.lineTo(premiumPoints[i].dx, premiumPoints[i].dy);
      }
    }
    canvas.drawPath(
      premiumPath, 
      Paint()..color = const Color(0xFFEF4444)..style = PaintingStyle.stroke..strokeWidth = 2
    );

    // 4. 绘制现金价值线 (柔性的复利指数曲线)
    final cvPath = Path();
    cvPath.moveTo(0, height);
    for (int i = 0; i < cvPoints.length; i++) {
      if (i == 0) {
        cvPath.lineTo(cvPoints[i].dx, cvPoints[i].dy);
      } else {
        cvPath.lineTo(cvPoints[i].dx, cvPoints[i].dy);
      }
    }

    // 现金价值下方着色发光蒙版
    final cvFillPath = Path.from(cvPath);
    cvFillPath.lineTo(width, height);
    cvFillPath.lineTo(0, height);
    cvFillPath.close();

    final fillPaint = Paint()
      ..shader = ui.Gradient.linear(
        Offset(0, 0), Offset(0, height), 
        [const Color(0xFFE5C07B).withValues(alpha: 0.3), const Color(0xFFE5C07B).withValues(alpha: 0.0)]
      )
      ..style = PaintingStyle.fill;
    canvas.drawPath(cvFillPath, fillPaint);

    final cvLinePaint = Paint()
      ..color = const Color(0xFFE5C07B)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round;
    canvas.drawPath(cvPath, cvLinePaint);

    // 5. 核心锚点:盈亏平衡点 (Break-even Intersection Point)
    if (breakevenPoint != null) {
      // 绘制交点光晕
      canvas.drawCircle(
        breakevenPoint, 
        8, 
        Paint()..color = const Color(0xFFE5C07B).withValues(alpha: 0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4)
      );
      // 绘制实体交点
      canvas.drawCircle(breakevenPoint, 4, Paint()..color = Colors.white);
      
      // 绘制垂直引导线
      _drawDashedLine(
        canvas, 
        Offset(breakevenPoint.dx, breakevenPoint.dy), 
        Offset(breakevenPoint.dx, height), 
        Paint()..color = const Color(0xFFE5C07B)..strokeWidth = 1
      );

      // 绘制平衡点气泡标签
      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(breakevenPoint.dx - 40, breakevenPoint.dy - 35, 80, 24), 
        const Radius.circular(4)
      );
      canvas.drawRRect(rect, Paint()..color = const Color(0xFFE5C07B));
      _drawText(canvas, '回本达成', Offset(breakevenPoint.dx - 24, breakevenPoint.dy - 30), Colors.black, fontWeight: FontWeight.bold);
    }
  }

  void _drawText(Canvas canvas, String text, Offset offset, Color color, {FontWeight fontWeight = FontWeight.normal}) {
    final tp = TextPainter(
      text: TextSpan(text: text, style: TextStyle(color: color, fontSize: 11, fontWeight: fontWeight)),
      textDirection: ui.TextDirection.ltr,
    );
    tp.layout();
    tp.paint(canvas, offset);
  }

  void _drawDashedLine(Canvas canvas, Offset p1, Offset p2, Paint paint) {
    const double dashWidth = 4;
    const double dashSpace = 4;
    double startY = p1.dy;
    while (startY < p2.dy) {
      canvas.drawLine(Offset(p1.dx, startY), Offset(p1.dx, startY + dashWidth), paint);
      startY += dashWidth + dashSpace;
    }
  }

  @override
  bool shouldRepaint(covariant _ActuarialCurvePainter oldDelegate) {
    return oldDelegate.matrix != matrix || oldDelegate.breakevenYear != breakevenYear;
  }
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐