开源鸿蒙跨平台Flutter开发:国寿险收益速算表系统:基于 Flutter 的金融精算模型与 IRR 收益率动态测绘架构
本文介绍了基于Flutter框架开发的国寿险收益速算系统,该系统通过领域驱动设计重构了传统寿险的精算模型。核心创新点包括: 采用二分逼近法解算内部收益率(IRR)方程,解决了传统算术平均收益率无法反映真实收益的问题 构建了动态现金流折现模型,精确计算保单现金价值随时间的增长曲线 实现了可视化交互界面,用户可直观查看不同年限下的盈亏平衡点和复利收益 系统采用DDD架构设计,将精算逻辑封装为独立引擎,
欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
实验效果


大额度效果

国寿险收益速算表系统:基于 Flutter 的金融精算模型与 IRR 收益率动态测绘架构
一、 引言:传统寿险利益演示的黑盒化危机
在购买如“增额终身寿险”、“年金险”等带有强金融属性的人寿保险时,消费者最核心的诉求往往只有两个:
- 多久能够回本(现金价值突破累计缴纳保费)?
- 长期的真实复利收益到底是多少?
由于保险公司在保单前几年需要扣除高额的初始费用、佣金以及身故风险保费,这导致前期的现金价值极低(退保即面临大幅亏损)。而当缴费期满后,现金价值将开始按照“预定利率”进行指数级的纯复利滚存。
这种非线性的现金流,导致传统的算术平均收益率完全失效。为了揭开这层算术伪装,我们必须引入金融数学界唯一的“照妖镜”——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×NN−t+1(t≤N)
则在第 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={(CVt−1+P(1−α(t)))×(1+r)CVt−1×(1+r)if t≤Nif t>N
当 C V t ≥ ∑ P CV_t \ge \sum P CVt≥∑P 时,即达成金融学意义上的“盈亏平衡(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=0∑min(N,T)−1(1+IRR)t−P+(1+IRR)TCVT=0
上述方程是一个高次多项式,无法通过常规代数求出解析解。在本文工程中,我们采用了计算几何学中最暴力的“二分逼近法(Bisection Method)”在 CPU 中对其进行 100 100 100 次极限强行逼近求解。
三、 系统领域驱动架构 (DDD) 与微观演算图谱
3.1 精算聚合根 UML 类图抽象
我们将精算过程严格地封闭在了 ActuarialEngine 中,任何 UI 层的滑动都不会直接污染底层计算公式,而是重新生成一份只读的(Immutable)矩阵账本。
3.2 IRR 二分法解算器的极限收敛瀑布流
下方的 Flowchart 展示了在我们的 Dart 引擎中,IRR 是如何从一片未知的混沌区间 [ − 99 % , + 100 % ] [-99\%, +100\%] [−99%,+100%] 中通过疯狂试探,最终锁定那个让 NPV 归零的“绝对真理数值”的。
四、 核心代码解剖学:金融级客户端的底层撕裂
要让枯燥的数字在手机上起舞,不仅需要极度严谨的代数学底蕴,更需要榨干 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) limt→100IRR(t) | 增额终身寿险最后无限逼近于定价复利(如 3.0%),体现抗击长期通胀的基石稳定性。 | float_limit_irr_300 |
| 退保绝对差损比 | ( C V t − ∑ P ) / ∑ P (CV_t - \sum P) / \sum P (CVt−∑P)/∑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;
}
}
更多推荐




所有评论(0)