111
This commit is contained in:
parent
260a4df818
commit
ecf000e466
|
|
@ -1089,80 +1089,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
Widget _buildRichText(String content, ColorScheme colorScheme) {
|
||||
final spans = <TextSpan>[];
|
||||
final regex = RegExp(r'\*\*(.*?)\*\*|__(.*?)__|`(.*?)`|\[(.*?)\]');
|
||||
int lastIndex = 0;
|
||||
// 递归解析文本,支持嵌套格式
|
||||
final spans = _parseRichText(content, colorScheme);
|
||||
|
||||
for (final match in regex.allMatches(content)) {
|
||||
if (match.start > lastIndex) {
|
||||
spans.add(TextSpan(
|
||||
text: content.substring(lastIndex, match.start),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if (match.group(1) != null) {
|
||||
spans.add(TextSpan(
|
||||
text: match.group(1),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
} else if (match.group(2) != null) {
|
||||
spans.add(TextSpan(
|
||||
text: match.group(2),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
));
|
||||
} else if (match.group(3) != null) {
|
||||
spans.add(TextSpan(
|
||||
text: match.group(3),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.primary,
|
||||
height: 1.4,
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
),
|
||||
));
|
||||
} else if (match.group(4) != null) {
|
||||
spans.add(TextSpan(
|
||||
text: match.group(4),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.primary,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
lastIndex = match.end;
|
||||
}
|
||||
|
||||
if (lastIndex < content.length) {
|
||||
spans.add(TextSpan(
|
||||
text: content.substring(lastIndex),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if (spans.isEmpty) {
|
||||
if (spans.isEmpty || spans.length == 1 && spans[0].text == content) {
|
||||
return Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
|
|
@ -1182,6 +1112,196 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 递归解析富文本,支持嵌套格式
|
||||
///
|
||||
/// 支持的格式:
|
||||
/// - `**粗体**` - 加粗文本
|
||||
/// - `__斜体__` - 斜体文本
|
||||
/// - `` `代码` `` - 等宽字体代码
|
||||
/// - `[高亮]` - 主题色高亮
|
||||
///
|
||||
/// 支持嵌套,例如:
|
||||
/// - `__在[学生管理]中添加详情页__` - 斜体中嵌套高亮
|
||||
/// - `[高亮**粗体**文本]` - 高亮中嵌套粗体
|
||||
/// - `` `代码**粗体**` `` - 代码中嵌套粗体
|
||||
///
|
||||
/// 处理逻辑:
|
||||
/// 1. 找到所有格式标记
|
||||
/// 2. 识别嵌套关系(被其他格式完全包围的为内层)
|
||||
/// 3. 优先处理最外层格式,递归处理内层内容
|
||||
/// 4. 继续处理剩余文本
|
||||
List<TextSpan> _parseRichText(String content, ColorScheme colorScheme) {
|
||||
final spans = <TextSpan>[];
|
||||
|
||||
// 定义格式标记:使用严格的正则表达式,避免匹配不完整的标记
|
||||
// 优先级:[] > ` > ** > __ (内层格式优先)
|
||||
final patterns = [
|
||||
(regex: RegExp(r'\[([^\]]+)\]'), type: 'highlight'), // [高亮] - 不包含 ] 的内容
|
||||
(regex: RegExp(r'`([^`]+)`'), type: 'code'), // `代码` - 不包含 ` 的内容
|
||||
(regex: RegExp(r'\*\*([^*]+(?:\*(?!\*)[^*]*)*)\*\*'), type: 'bold'), // **粗体** - 不包含 ** 的内容
|
||||
(regex: RegExp(r'__([^_]+(?:_(?!_)[^_]*)*)__'), type: 'italic'), // __斜体__ - 不包含 __ 的内容
|
||||
];
|
||||
|
||||
// 找到所有匹配的格式标记
|
||||
final allMatches = <({Match match, String type, int priority})>[];
|
||||
for (int i = 0; i < patterns.length; i++) {
|
||||
final pattern = patterns[i];
|
||||
for (final match in pattern.regex.allMatches(content)) {
|
||||
// 确保匹配的内容不为空
|
||||
if (match.group(1) != null && match.group(1)!.isNotEmpty) {
|
||||
allMatches.add((match: match, type: pattern.type, priority: i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配,返回普通文本
|
||||
if (allMatches.isEmpty) {
|
||||
return [
|
||||
TextSpan(
|
||||
text: content,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 按位置排序,找到最左边的匹配
|
||||
allMatches.sort((a, b) {
|
||||
// 先按开始位置排序
|
||||
if (a.match.start != b.match.start) {
|
||||
return a.match.start.compareTo(b.match.start);
|
||||
}
|
||||
// 如果开始位置相同,按优先级排序(高优先级在前,即 [] 和 ` 优先)
|
||||
if (a.priority != b.priority) {
|
||||
return a.priority.compareTo(b.priority);
|
||||
}
|
||||
// 如果优先级也相同,按结束位置排序(短的在前)
|
||||
return a.match.end.compareTo(b.match.end);
|
||||
});
|
||||
|
||||
// 找到第一个不被其他格式完全包围的匹配(最外层的)
|
||||
Match? selectedMatch;
|
||||
String? selectedType;
|
||||
|
||||
for (final item in allMatches) {
|
||||
bool isNested = false;
|
||||
// 检查是否被其他格式完全包围(开始位置更早且结束位置更晚)
|
||||
for (final other in allMatches) {
|
||||
if (other.match != item.match &&
|
||||
other.match.start <= item.match.start &&
|
||||
other.match.end >= item.match.end &&
|
||||
(other.match.start < item.match.start || other.match.end > item.match.end)) {
|
||||
isNested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 选择第一个不被嵌套的匹配
|
||||
if (!isNested) {
|
||||
selectedMatch = item.match;
|
||||
selectedType = item.type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到(理论上不应该发生),使用第一个匹配
|
||||
selectedMatch ??= allMatches.first.match;
|
||||
selectedType ??= allMatches.first.type;
|
||||
|
||||
int lastIndex = 0;
|
||||
|
||||
// 添加匹配前的普通文本
|
||||
if (selectedMatch.start > lastIndex) {
|
||||
spans.add(TextSpan(
|
||||
text: content.substring(lastIndex, selectedMatch.start),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 处理匹配到的格式内容(递归解析,支持嵌套)
|
||||
final matchedContent = selectedMatch.group(1)!;
|
||||
final nestedSpans = _parseRichText(matchedContent, colorScheme);
|
||||
|
||||
// 应用当前格式的样式
|
||||
TextStyle baseStyle;
|
||||
switch (selectedType) {
|
||||
case 'bold':
|
||||
baseStyle = TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
break;
|
||||
case 'italic':
|
||||
baseStyle = TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontStyle: FontStyle.italic,
|
||||
);
|
||||
break;
|
||||
case 'code':
|
||||
baseStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.primary,
|
||||
height: 1.4,
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
);
|
||||
break;
|
||||
case 'highlight':
|
||||
baseStyle = TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.primary,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
baseStyle = TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
);
|
||||
}
|
||||
|
||||
// 如果嵌套解析有结果,应用样式;否则直接使用匹配的内容
|
||||
if (nestedSpans.isNotEmpty &&
|
||||
(nestedSpans.length > 1 || (nestedSpans.isNotEmpty && nestedSpans[0].text != matchedContent))) {
|
||||
// 有嵌套格式,应用样式到所有嵌套的spans
|
||||
final styledSpans = nestedSpans.map((span) {
|
||||
return TextSpan(
|
||||
text: span.text,
|
||||
style: span.style?.merge(baseStyle) ?? baseStyle,
|
||||
children: span.children,
|
||||
);
|
||||
}).toList();
|
||||
spans.addAll(styledSpans);
|
||||
} else {
|
||||
// 无嵌套格式,直接应用样式
|
||||
spans.add(TextSpan(
|
||||
text: matchedContent,
|
||||
style: baseStyle,
|
||||
));
|
||||
}
|
||||
|
||||
// 继续解析剩余部分
|
||||
lastIndex = selectedMatch.end;
|
||||
if (lastIndex < content.length) {
|
||||
final remainingSpans = _parseRichText(content.substring(lastIndex), colorScheme);
|
||||
spans.addAll(remainingSpans);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) {
|
||||
final bool showRetryButton = _downloadedFilePath != null &&
|
||||
!_isDownloading &&
|
||||
|
|
|
|||
Loading…
Reference in New Issue