欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
绪论:一个突变字符引发的"系统惨案"
在计算机程序设计的世界里,我们习惯了 0 与 1 的绝对确信。然而,当软件工程跨入临床医学与计算生物学(Computational Biology)的殿堂,我们所面对的输入往往是充满噪声与人为失误的"脏数据"。
在实际的核酸测序(NGS)工作流中,一条由测序仪输出的巨大 FASTA 文件在网络传输、人类手工编辑甚至制表软件的篡改下,极其容易混入致命的污染。想象一下,一串长达几万字符的 DNA 序列中,因为研究员疲惫的误触,混入了一个字母 Z,或者将代表占位符的数字 0 敲成了英文字母 O。如果上游系统的解析引擎缺乏防御性编程(Defensive Programming)与格式强校验机制,这些被污染的游离数据将堂而皇之地流入下游的三维蛋白质结构预测模型(如 AlphaFold)或比对矩阵计算中。
其后果,轻则导致连续运行了数十小时的 GPU 阵列进程在中途无征兆地触发"段错误(Segmentation Fault)"并全盘崩溃;重则输出极具误导性的生物医学结论。
本篇文章旨在确立数据治理的"第一法则":永远不要信任外部输入 。我们将依托 Flutter 构建一个极具交互性的"生信数据清洗与校验中枢",利用基于离散数学确立的正则表达式(Regular Expressions, RegExp)来构建绝对的规则滤网,并依靠 Dart 强大的面向对象特性建立一套层级分明的领域异常捕获体系(Custom Exception Hierarchy)。
演示效果

测试数据·人类 p53 抑癌基因(关联多种肿瘤,临床常用检测靶点)
>NP_000537.3 Homo sapiens tumor protein p53 (TP53), mRNA (partial cds)
ATGGAGGAGCCGCAGTCAGATCCTAGCGTCGAGCCCCCTCTGAGTCAGGAAACATTTTCAGACCTATGG
AATCATTGCATCAGCCATGGCAGTGGTGGTGACGACACGCTTCCCTGGATTGGCAGCCAGACTGCCTTCC
GGGTCACTGCCATGGAGGAGCCGCAGTCAGATCCTAGCGTCGAGCCCCCTCTGAGTCAGGAAACATTTTC
AGACCTATGGAATCATTGCATCAGCCATGGCAGTGGTGGTGACGACACGCTTCCCTGGATTGGCAGCCAG
ACTGCCTTCCGGGTCACTGCCATGAGCGCTGCTCAGATAGCGATGGTGGTGGTGGCGGAGGGCGCGGAGG
CGGCGGAGAGCTTCCTGAGCCCGGAGGCGGAGGCGGAGGAGGGAGAGAGCTTCCTGAGCCCGGAGGCGGA
GGCGGAGGAGGGAGAG

一、 数据边界的数学定义:FASTA 规范与正则抽象
为了让计算机能够实施精准打击,我们首先需要利用数学语言为 FASTA 格式确立严苛的合法字符域。
1.1 序列集合的离散数学论述
对于标准的脱氧核糖核酸(DNA)序列,其有效的符号字母表(Alphabet)可以表示为一个有限集合 Σ D N A \Sigma_{DNA} ΣDNA:
Σ D N A = { A , T , C , G , N } \Sigma_{DNA} = \{ A, T, C, G, N \} ΣDNA={A,T,C,G,N}
其中, N N N 代表测序仪无法精确分辨的未知核苷酸(Any Nucleotide)。
而对于 FASTA 文件的格式结构,其遵循着极简但严格的二元逻辑流:
- 标识头(Header) :以单个的右尖括号
>起手,随后紧跟一串不含空格的唯一序列标识符(ID),之后可跟随可选的描述文本。 - 序列体(Sequence Body) :可以是一行或多行,字符域必须严格落入 Σ \Sigma Σ 集合的子集中。
1.2 正则表达式 (RegExp) 引擎的搭建
基于上述理论规范,我们将抽象域转化为计算域,在 FastaValidationEngine 核心中构建了以下两大正则表达式:
dart
// 选自 main.dart 核心校验引擎类 FastaValidationEngine
class FastaValidationEngine {
// 正则规则 1:FASTA 标头校验防线
// 解释:
// ^> : 必须以 > 开头
// [a-zA-Z0-9_\|\-\.]+ : ID 只能包含字母、数字,以及下划线、短横线、竖线和点号
// (?:\s+.*)?$ : ID 后必须有一个空格,然后才允许附带杂项描述信息直到行尾
static final RegExp _headerRegex = RegExp(r'^>[a-zA-Z0-9_\|\-\.]+(?:\s+.*)?$');
// 正则规则 2:DNA 碱基合法性防线
// 解释:
// ^ : 行首锚点
// [ACGTNacgtn\s]+ : 只允许合法的5种碱基(大小写兼容)及换行回车等空白符
// $ : 行尾锚点
static final RegExp _dnaRegex = RegExp(r'^[ACGTNacgtn\s]+$');
// ...
}
任何企图蒙混过关的游离字符,在面对底层 C++ 构建的高速正则匹配机状态网(DFA/NFA)时,都将原形毕露。
二、 构建领域异常体系 (Custom Exception Hierarchy)
发现错误固然重要,但如何将错误的病理信息、上下文、甚至事发地点的精准坐标(行号)无损地传递给上游的 UI 层,这是一门软件工程学的高阶艺术。
直接抛出字符串(如 throw '格式错了')是低级且不负责任的。我们采用面向对象多态法则,绘制并实现了一套具备高度扩展性的 UML 异常树。
<<interface>>
Exception
<<abstract>>
FastaParseException
+String message
+int lineNumber
+String problematicLine
+toString() : String
InvalidHeaderException
+异常: 缺少>符号或ID包含非法空白
InvalidSequenceCharacterException
+String invalidChar
+异常: 序列中存在如Z, X, O的污染字符
EmptySequenceException
+异常: 头标识下方没有任何实体核酸数据
通过这套基于继承机制建立的错误类体系,我们就相当于给每一个程序崩溃赋予了"病理诊断切片"。
2.1 异常基类的降维封装代码实现
dart
// 选自异常定义层代码
abstract class FastaParseException implements Exception {
final String message; // 人类可读的诊断断言
final int lineNumber; // 出错的物理行号(精准定位)
final String problematicLine; // 脏数据快照提取
FastaParseException(this.message, this.lineNumber, this.problematicLine);
@override
String toString() => '[$runtimeType - 行 $lineNumber] $message\n错误内容: $problematicLine';
}
/// 序列包含非法字符异常:独有字段记录具体是哪个"脏字符"引发了崩溃
class InvalidSequenceCharacterException extends FastaParseException {
final String invalidChar;
InvalidSequenceCharacterException(super.message, super.lineNumber, super.problematicLine, this.invalidChar);
}
三、 诊断引擎流水线剖析:防御性编程实战
有了正则和异常类,我们将在 validateFastaData 函数中部署一套类似于流水线车间的安检探测门。
我们没有采用一旦发现错误立刻中断(Fail-Fast)的策略,因为在庞杂的数据清理中,用户希望**"一次性得知所有的错误并批量修正"**。因此,函数返回的是一个收集了各种错误对象的 List<FastaParseException>。
:
系统维护了一个极为微小的双节点状态机( hasSeenHeader 与 hasCurrentSequenceData)。当循环扫过一行文本时:如果这是一行以 > 开头的头信息,引擎会立刻回头审查上一个标头下方是否真的拥有核酸数据。如果是一具没有肉体的空壳序列,引擎毫不犹豫地抛出 EmptySequenceException 登记造册。
安检通道二:脏碱基反向狙击 当文本进入实体序列扫描时,利用 _dnaRegex.hasMatch(currentLine) 进行瞬间的宏观甄别。如果反馈为 false,代表内有污染。为了给 UI 提供最高级别的交互反馈,我们并未就此罢休,而是运用极耗性能但必须执行的遍历算法,将行内那个隐匿的脏字符揪出来:
dart
// 选自核心循环判定区域
if (!_dnaRegex.hasMatch(currentLine)) {
// 为了给用户精准反馈,我们在确诊有病变后,进行深度开胸手术提取非法源
String badChar = '未知';
for(int charIdx = 0; charIdx < currentLine.length; charIdx++) {
String c = currentLine[charIdx];
if (!RegExp(r'[ACGTNacgtn\s]').hasMatch(c)) {
badChar = c; // 成功抓获脏碱基
break;
}
}
errorLog.add(InvalidSequenceCharacterException(
'污染数据异常:检测到非法的 DNA/RNA 碱基字符。',
currentLineNum,
currentLine,
badChar
));
}
四、 人机交互层:可视化容错体验的极致追求
当冰冷刻板的正则判断机制遇到终端用户时,UI 层必须表现得极度克制、优雅且具有安抚性。
在这个项目中,我们在单一界面内打造了三大高强度交互模块。
- 一键污染注入模拟 :为了便于测试,顶部的工具栏内置了模拟器。可以一键输入标准核酸结构,亦可注入包含游离孤儿头、含有字母
Z污染等劣质数据,供下游管线体验真实挫败。 - 文本输入框的动态反馈 :当数据纯净时,边界呈现生命之树的苍翠绿(Green);一旦捕获到
_validationErrors!.isNotEmpty,整个边界框立即渗出警戒红,并在右上角空投下醒目的危急中断徽章(Chip)。 - 动画展开的重症诊断监控台:这部分是全场最佳的技术高光。
4.1 异常反馈展板与富文本 (RichText) 的精准标红
当程序收集到异常数组时,底部的控制面板通过 SizeTransition 动画伴随优雅的贝塞尔曲线滑出。对于展示给科研人员的报告,我们不能只丢下一句"格式错误"。
通过解析底层传来的多态异常类,我们赋予每种异常独特的病状徽标,更利用了 Flutter 极其强大的 RichText 组件,对案发第一现场进行了还原:
dart
// 选自 UI 层:_highlightBadCharacter 渲染器
TextSpan _highlightBadCharacter(FastaParseException error) {
if (error is InvalidSequenceCharacterException) {
final String raw = error.problematicLine; // 获取全行源文本
final String bad = error.invalidChar; // 锁定那个脏字符
final int idx = raw.indexOf(bad);
if (idx != -1) {
// 利用 TextSpan 进行字符串切割,犹如用手术刀剥离病灶
return TextSpan(
children: [
TextSpan(text: raw.substring(0, idx)),
TextSpan(
text: raw.substring(idx, idx + 1),
// 唯独对这个非法的字符施加耀眼的红底白字高亮,使其在屏幕上无处遁形
style: const TextStyle(color: Colors.white, backgroundColor: Colors.red, fontWeight: FontWeight.bold)
),
TextSpan(text: raw.substring(idx + 1)),
]
);
}
}
return TextSpan(text: error.problematicLine);
}
这种让异常变得"显而易见"的可视化方案,极大地消解了实验室新手面对庞杂文件时束手无策的绝望感。
五、 后续发展蓝图:从内存态向持久态的跨越
在本篇博客中,我们建立了一套毫无破绽的安检闸门。任何序列只有先通过这套结合了 RegExp 暴力算法与 Custom Exception 面向对象体系的洗礼,才配享有被后续医学算法计算的资格。
但这依然属于基于应用内存即时层面的防御。当设备断电或是进程遭到系统回收,这些珍贵的数据将烟消云散。在接下来的阶段,我们将正式拥抱系统本地的持久化方案。
数据如流水,而我们所撰写的异常捕获与正则体系,便是扼守江河的坚实大坝,用秩序梳理混沌,以确定驱逐未知。人类在解密基因图谱的路上,离不开这极致入微的把控与坚持。
完整代码
bash
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const FastaValidationApp());
}
/// ---------------------------------------------------------------------------
/// 全局应用入口
/// ---------------------------------------------------------------------------
class FastaValidationApp extends StatelessWidget {
const FastaValidationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '生信数据清洗与校验中枢',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2C3E50), // 沉稳的实验室深石板灰
primary: const Color(0xFF2C3E50),
secondary: const Color(0xFFE74C3C), // 异常警告红
surface: const Color(0xFFF8F9FA),
background: const Color(0xFFECF0F1),
),
textTheme: const TextTheme(
bodyMedium: TextStyle(fontSize: 16, height: 1.6),
),
),
home: const FastaValidationScreen(),
);
}
}
/// ---------------------------------------------------------------------------
/// 领域异常类体系 (Custom Exception Hierarchy)
/// ---------------------------------------------------------------------------
abstract class FastaParseException implements Exception {
final String message;
final int lineNumber;
final String problematicLine;
FastaParseException(this.message, this.lineNumber, this.problematicLine);
@override
String toString() => '[$runtimeType - 行 $lineNumber] $message\n错误内容: $problematicLine';
}
/// 头部标识符不规范异常
class InvalidHeaderException extends FastaParseException {
InvalidHeaderException(super.message, super.lineNumber, super.problematicLine);
}
/// 序列包含非法字符异常 (如 DNA 中出现了 Z 或 O)
class InvalidSequenceCharacterException extends FastaParseException {
final String invalidChar;
InvalidSequenceCharacterException(super.message, super.lineNumber, super.problematicLine, this.invalidChar);
}
/// 序列为空异常
class EmptySequenceException extends FastaParseException {
EmptySequenceException(super.message, super.lineNumber, super.problematicLine);
}
/// ---------------------------------------------------------------------------
/// 核心校验引擎层:基于正则表达式 (RegExp)
/// ---------------------------------------------------------------------------
class FastaValidationEngine {
// 正则规则 1:FASTA 头必须以 > 开始,且后续紧跟非空白字母数字或 - | _ . 等有效 ID 字符
static final RegExp _headerRegex = RegExp(r'^>[a-zA-Z0-9_\|\-\.]+(?:\s+.*)?$');
// 正则规则 2:DNA 序列只能包含 A, C, G, T, N (大小写均可),以及系统换行空格
static final RegExp _dnaRegex = RegExp(r'^[ACGTNacgtn\s]+$');
/// 执行高强度的数据清洗与校验,返回收集到的异常列表或空列表(代表通过)
static Future<List<FastaParseException>> validateFastaData(String rawData) async {
final List<FastaParseException> errorLog = [];
final List<String> lines = rawData.split('\n');
bool hasSeenHeader = false;
bool hasCurrentSequenceData = false;
int sequenceStartIndex = -1;
// 逐行执行防御性编程校验
for (int i = 0; i < lines.length; i++) {
String currentLine = lines[i].trim();
int currentLineNum = i + 1;
// 忽略纯空行
if (currentLine.isEmpty) continue;
if (currentLine.startsWith('>')) {
// 发现新序列头前,先校验上一个序列是否为空
if (hasSeenHeader && !hasCurrentSequenceData) {
errorLog.add(EmptySequenceException(
'严重逻辑异常:序列标头下缺乏对应的核酸实体数据。',
sequenceStartIndex,
lines[sequenceStartIndex - 1]
));
}
// 校验 Header 命名规范
if (!_headerRegex.hasMatch(currentLine)) {
errorLog.add(InvalidHeaderException(
'格式异常:序列标识符(ID)含有非法控制字符或空格断层。',
currentLineNum,
currentLine
));
}
hasSeenHeader = true;
hasCurrentSequenceData = false;
sequenceStartIndex = currentLineNum;
} else {
// 如果还没有头就开始写序列数据,属于严重截断错误
if (!hasSeenHeader) {
errorLog.add(InvalidHeaderException(
'结构异常:序列数据游离,未寻找到归属的 ">" 标识符。',
currentLineNum,
currentLine
));
hasSeenHeader = true; // 强制设为 true 防止后续全部报错
}
// 利用高强度正则匹配非法碱基
if (!_dnaRegex.hasMatch(currentLine)) {
// 倘若正则未通过,为了给用户精准反馈,手动提取出究竟是哪个字符非法
String badChar = '未知';
for(int charIdx = 0; charIdx < currentLine.length; charIdx++) {
String c = currentLine[charIdx];
if (!RegExp(r'[ACGTNacgtn\s]').hasMatch(c)) {
badChar = c;
break; // 抓到一个即可
}
}
errorLog.add(InvalidSequenceCharacterException(
'污染数据异常:检测到非法的 DNA/RNA 碱基字符。',
currentLineNum,
currentLine,
badChar
));
}
hasCurrentSequenceData = true;
}
// 模拟微小的异步开销,防止主线程卡顿
if (i % 100 == 0) await Future.delayed(Duration.zero);
}
// 文件结尾校验
if (hasSeenHeader && !hasCurrentSequenceData) {
errorLog.add(EmptySequenceException(
'文件截断异常:最后一条序列缺乏核酸实体数据。',
sequenceStartIndex,
lines[sequenceStartIndex - 1]
));
}
return errorLog;
}
}
/// ---------------------------------------------------------------------------
/// UI 层:高交互式的数据清洗控制台
/// ---------------------------------------------------------------------------
class FastaValidationScreen extends StatefulWidget {
const FastaValidationScreen({super.key});
@override
State<FastaValidationScreen> createState() => _FastaValidationScreenState();
}
class _FastaValidationScreenState extends State<FastaValidationScreen> with SingleTickerProviderStateMixin {
final TextEditingController _inputController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isProcessing = false;
List<FastaParseException>? _validationErrors;
// 底部异常展板动画控制器
late AnimationController _panelController;
late Animation<double> _panelAnimation;
@override
void initState() {
super.initState();
_panelController = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
_panelAnimation = CurvedAnimation(parent: _panelController, curve: Curves.easeInOutCubic);
}
@override
void dispose() {
_inputController.dispose();
_focusNode.dispose();
_panelController.dispose();
super.dispose();
}
/// 注入模拟的【干净】数据
void _injectCleanMockData() {
_inputController.text = '''>Sequence_001_WildType
ATCGATCGATCGTACGATCG
CGATCGATCGTACGATCGTA
>Sequence_002_Mutant
ATCGATCGNNNNTACGATCG''';
_clearErrors();
}
/// 注入模拟的【严重污染】数据
void _injectDirtyMockData() {
_inputController.text = '''> Sequence_Bad_Header_With_Space
ATCGATCGATCGTACGATCG
>Sequence_Missing_Data
>Sequence_With_Invalid_Chars
ATCGZXCGATCGTACGATCG
ATCGATCGATCGTACGATCO'''; // Z, X, 和字母O都是非法的
_clearErrors();
}
void _clearErrors() {
setState(() {
_validationErrors = null;
});
_panelController.reverse();
}
/// 触发正则校验流水线
Future<void> _runValidation() async {
final String rawText = _inputController.text;
if (rawText.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先输入或加载 FASTA 数据序列')),
);
return;
}
_focusNode.unfocus(); // 收起键盘
setState(() {
_isProcessing = true;
_validationErrors = null;
});
_panelController.reverse();
try {
// 调用校验引擎,并捕获未知的系统级崩溃
final errors = await FastaValidationEngine.validateFastaData(rawText);
setState(() {
_validationErrors = errors;
});
// 如果有错误,展开底部的错误展板
if (errors.isNotEmpty) {
_panelController.forward();
}
} catch (e) {
// 顶级防御:捕获校验引擎本身的内存溢出或其他未知崩溃
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('解析引擎发生系统级崩溃: $e'), backgroundColor: Colors.red),
);
} finally {
setState(() {
_isProcessing = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
appBar: AppBar(
title: const Text('FASTA 序列质量控制网关', style: TextStyle(fontWeight: FontWeight.w700, color: Colors.white)),
backgroundColor: Theme.of(context).colorScheme.primary,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.white),
tooltip: '清空画布',
onPressed: () {
_inputController.clear();
_clearErrors();
},
)
],
),
body: SafeArea(
child: Column(
children: [
// 顶层操作区:数据注入
_buildActionToolbar(),
// 中间层:高亮编辑器区域
Expanded(
child: _buildEditorArea(),
),
// 底层:动态高度的异常分析看板 (通过动画展开)
SizeTransition(
sizeFactor: _panelAnimation,
axisAlignment: -1.0,
child: _buildErrorPanel(),
),
],
),
),
);
}
/// 构建工具栏交互按钮组
Widget _buildActionToolbar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Colors.white,
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _injectCleanMockData,
icon: const Icon(Icons.verified, color: Colors.green),
label: const Text('载入标准测序数据'),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.green),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _injectDirtyMockData,
icon: const Icon(Icons.coronavirus, color: Colors.orange),
label: const Text('注入高污染测试集'),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.orange),
),
),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _runValidation,
icon: _isProcessing
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.security),
label: const Text('执行深度校验'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
),
),
],
),
);
}
/// 构建核心文本交互编辑器
Widget _buildEditorArea() {
// 根据校验状态动态变异边框颜色
Color borderColor = Colors.grey.shade300;
if (_validationErrors != null) {
borderColor = _validationErrors!.isEmpty ? Colors.green : Colors.red.shade400;
}
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
)
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
children: [
TextField(
controller: _inputController,
focusNode: _focusNode,
maxLines: null,
expands: true,
style: const TextStyle(fontFamily: 'monospace', fontSize: 15, letterSpacing: 1.2),
decoration: const InputDecoration(
contentPadding: EdgeInsets.all(24),
border: InputBorder.none,
hintText: '请将 FASTA 格式数据粘贴至此处...\n例如:\n>Seq_1\nATCG...',
),
onChanged: (val) {
// 如果内容被篡改,立即收起并销毁之前的报错面板
if (_validationErrors != null) _clearErrors();
},
),
// 右上角浮动状态指示器
if (_validationErrors != null)
Positioned(
top: 16,
right: 16,
child: _validationErrors!.isEmpty
? const Chip(
avatar: Icon(Icons.check_circle, color: Colors.white),
label: Text('校验通过, 数据纯净', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
backgroundColor: Colors.green,
)
: Chip(
avatar: const Icon(Icons.error, color: Colors.white),
label: Text('检出 ${_validationErrors!.length} 处危急阻断', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
backgroundColor: Colors.red.shade600,
)
)
],
),
),
);
}
/// 构建交互式底侧异常审查分析台
Widget _buildErrorPanel() {
if (_validationErrors == null || _validationErrors!.isEmpty) return const SizedBox.shrink();
return Container(
height: 280,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.red.shade200, width: 4)),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, -5))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 标题栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
color: Colors.red.shade50,
child: Row(
children: [
Icon(Icons.bug_report, color: Colors.red.shade700),
const SizedBox(width: 8),
Text('序列污染分析报告 (${_validationErrors!.length} 处)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.red.shade900)),
],
),
),
// 滚动错误列表
Expanded(
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _validationErrors!.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final error = _validationErrors![index];
return _buildErrorItem(error);
},
),
),
],
),
);
}
/// 构建单条错误报告的可交互 Cell
Widget _buildErrorItem(FastaParseException error) {
IconData iconData = Icons.error_outline;
Color iconColor = Colors.grey;
if (error is InvalidHeaderException) {
iconData = Icons.title;
iconColor = Colors.orange.shade700;
} else if (error is InvalidSequenceCharacterException) {
iconData = Icons.biotech;
iconColor = Colors.red.shade600;
} else if (error is EmptySequenceException) {
iconData = Icons.link_off;
iconColor = Colors.purple.shade500;
}
return ListTile(
leading: CircleAvatar(
backgroundColor: iconColor.withOpacity(0.1),
child: Icon(iconData, color: iconColor),
),
title: RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black87, fontSize: 15),
children: [
TextSpan(text: '第 ${error.lineNumber} 行: ', style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: error.message, style: TextStyle(color: iconColor)),
]
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey.shade300),
),
child: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'monospace', color: Colors.black54),
children: [
const TextSpan(text: '源文本 > '),
_highlightBadCharacter(error),
]
),
),
),
),
);
}
/// 富文本高亮渲染器:如果是字符异常,精准标红引发崩溃的那个脏碱基
TextSpan _highlightBadCharacter(FastaParseException error) {
if (error is InvalidSequenceCharacterException) {
final String raw = error.problematicLine;
final String bad = error.invalidChar;
final int idx = raw.indexOf(bad);
if (idx != -1) {
return TextSpan(
children: [
TextSpan(text: raw.substring(0, idx)),
TextSpan(
text: raw.substring(idx, idx + 1),
style: const TextStyle(color: Colors.white, backgroundColor: Colors.red, fontWeight: FontWeight.bold)
),
TextSpan(text: raw.substring(idx + 1)),
]
);
}
}
return TextSpan(text: error.problematicLine);
}
}