enhanced_text_editor.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_lucide/flutter_lucide.dart';
  4. import '../../models/element.dart';
  5. import '../../models/annotation.dart';
  6. import '../../theme/app_colors.dart';
  7. /// 增强的文本编辑器组件
  8. class EnhancedTextEditor extends StatefulWidget {
  9. final String text;
  10. final List<DocumentElement> elements;
  11. final List<Annotation> annotations;
  12. final Function(String selectedText)? onTextSelected;
  13. final Function(Annotation annotation)? onAnnotationAction;
  14. const EnhancedTextEditor({
  15. Key? key,
  16. required this.text,
  17. required this.elements,
  18. this.annotations = const [],
  19. this.onTextSelected,
  20. this.onAnnotationAction,
  21. }) : super(key: key);
  22. @override
  23. State<EnhancedTextEditor> createState() => _EnhancedTextEditorState();
  24. }
  25. class _EnhancedTextEditorState extends State<EnhancedTextEditor> {
  26. String? _selectedText;
  27. String _defaultSelectedText() {
  28. final t = widget.text.trim();
  29. if (t.isEmpty) return '';
  30. String firstLine = t.split('\n').first;
  31. // 切第一句
  32. if (firstLine.contains('。')) {
  33. firstLine = firstLine.split('。').first;
  34. }
  35. if (firstLine.length > 40) {
  36. firstLine = firstLine.substring(0, 40);
  37. }
  38. return firstLine;
  39. }
  40. @override
  41. Widget build(BuildContext context) {
  42. final String displaySelected =
  43. (_selectedText != null && _selectedText!.isNotEmpty)
  44. ? _selectedText!
  45. : _defaultSelectedText();
  46. return Column(
  47. mainAxisSize: MainAxisSize.min,
  48. crossAxisAlignment: CrossAxisAlignment.stretch,
  49. children: [
  50. // 固定显示的操作工具栏(无选择时使用默认文本)
  51. Container(
  52. margin: const EdgeInsets.only(bottom: 12),
  53. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  54. decoration: BoxDecoration(
  55. color: AppColors.primary.withOpacity(0.05),
  56. borderRadius: BorderRadius.circular(8),
  57. border: Border.all(color: AppColors.primary.withOpacity(0.2)),
  58. ),
  59. child: Row(
  60. children: [
  61. Expanded(
  62. child: Text(
  63. '选中: "$displaySelected"',
  64. style: TextStyle(
  65. fontSize: 13,
  66. color: AppColors.textPrimary,
  67. fontWeight: FontWeight.w500,
  68. ),
  69. maxLines: 1,
  70. overflow: TextOverflow.ellipsis,
  71. ),
  72. ),
  73. const SizedBox(width: 8),
  74. _buildToolbarButton(
  75. icon: LucideIcons.highlighter,
  76. label: '标记为要素',
  77. onTap: () {
  78. if (displaySelected.isNotEmpty) {
  79. widget.onTextSelected?.call(displaySelected);
  80. }
  81. },
  82. ),
  83. const SizedBox(width: 4),
  84. _buildToolbarButton(
  85. icon: LucideIcons.spell_check,
  86. label: '检查拼写',
  87. onTap: () {},
  88. ),
  89. const SizedBox(width: 4),
  90. _buildToolbarButton(
  91. icon: LucideIcons.sparkles,
  92. label: 'AI润色',
  93. onTap: () {},
  94. ),
  95. const SizedBox(width: 4),
  96. _buildToolbarButton(
  97. icon: LucideIcons.copy,
  98. label: '复制',
  99. onTap: () {
  100. if (displaySelected.isNotEmpty) {
  101. Clipboard.setData(ClipboardData(text: displaySelected));
  102. }
  103. },
  104. ),
  105. ],
  106. ),
  107. ),
  108. // 文本内容
  109. GestureDetector(
  110. onTap: () {},
  111. child: SingleChildScrollView(
  112. child: SelectableText.rich(
  113. TextSpan(
  114. children: _buildTextSpans(),
  115. style: const TextStyle(
  116. fontSize: 15,
  117. height: 1.8,
  118. color: AppColors.textPrimary,
  119. ),
  120. ),
  121. onSelectionChanged: (selection, cause) {
  122. if (selection.isValid && !selection.isCollapsed) {
  123. final text = widget.text.substring(
  124. selection.start,
  125. selection.end,
  126. );
  127. setState(() {
  128. _selectedText = text;
  129. });
  130. }
  131. },
  132. ),
  133. ),
  134. ),
  135. ],
  136. );
  137. }
  138. Widget _buildToolbarButton({
  139. required IconData icon,
  140. required String label,
  141. required VoidCallback onTap,
  142. }) {
  143. return Material(
  144. color: Colors.transparent,
  145. child: InkWell(
  146. onTap: onTap,
  147. borderRadius: BorderRadius.circular(6),
  148. child: Container(
  149. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
  150. child: Row(
  151. mainAxisSize: MainAxisSize.min,
  152. children: [
  153. Icon(icon, size: 14, color: AppColors.primary),
  154. const SizedBox(width: 4),
  155. Text(
  156. label,
  157. style: TextStyle(
  158. fontSize: 12,
  159. color: AppColors.primary,
  160. fontWeight: FontWeight.w500,
  161. ),
  162. ),
  163. ],
  164. ),
  165. ),
  166. ),
  167. );
  168. }
  169. List<TextSpan> _buildTextSpans() {
  170. final spans = <TextSpan>[];
  171. final text = widget.text;
  172. int lastIndex = 0;
  173. // 合并要素和批注,按位置排序
  174. final allMarkers = <_TextMarker>[];
  175. // 添加要素标记
  176. for (final element in widget.elements) {
  177. final index = text.indexOf(element.value, lastIndex);
  178. if (index != -1) {
  179. allMarkers.add(_TextMarker(
  180. start: index,
  181. end: index + element.value.length,
  182. type: _MarkerType.element,
  183. element: element,
  184. ));
  185. }
  186. }
  187. // 添加批注标记
  188. for (final annotation in widget.annotations) {
  189. final index = text.indexOf(annotation.text);
  190. if (index != -1) {
  191. allMarkers.add(_TextMarker(
  192. start: index,
  193. end: index + annotation.text.length,
  194. type: _MarkerType.annotation,
  195. annotation: annotation,
  196. ));
  197. }
  198. }
  199. // 按位置排序
  200. allMarkers.sort((a, b) => a.start.compareTo(b.start));
  201. // 构建文本片段
  202. for (final marker in allMarkers) {
  203. // 添加标记前的文本
  204. if (marker.start > lastIndex) {
  205. spans.add(TextSpan(text: text.substring(lastIndex, marker.start)));
  206. }
  207. // 添加标记文本
  208. if (marker.type == _MarkerType.element) {
  209. final element = marker.element!;
  210. final color = _getElementColor(element.type);
  211. spans.add(
  212. TextSpan(
  213. children: [
  214. WidgetSpan(
  215. alignment: PlaceholderAlignment.middle,
  216. child: Container(
  217. margin: const EdgeInsets.only(right: 4),
  218. padding:
  219. const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  220. decoration: BoxDecoration(
  221. color: color.withOpacity(0.15),
  222. borderRadius: BorderRadius.circular(4),
  223. border: Border.all(color: color.withOpacity(0.4)),
  224. ),
  225. child: Text(
  226. element.label,
  227. style: TextStyle(
  228. fontSize: 10,
  229. fontWeight: FontWeight.w600,
  230. color: color,
  231. ),
  232. ),
  233. ),
  234. ),
  235. TextSpan(
  236. text: element.value,
  237. style: TextStyle(
  238. backgroundColor: color.withOpacity(0.2),
  239. color: color,
  240. fontWeight: FontWeight.w600,
  241. decoration: TextDecoration.underline,
  242. decorationColor: color,
  243. ),
  244. mouseCursor: SystemMouseCursors.click,
  245. ),
  246. ],
  247. ),
  248. );
  249. } else if (marker.type == _MarkerType.annotation) {
  250. final annotation = marker.annotation!;
  251. spans.add(_buildAnnotationSpan(annotation));
  252. }
  253. lastIndex = marker.end;
  254. }
  255. // 添加剩余文本
  256. if (lastIndex < text.length) {
  257. spans.add(TextSpan(text: text.substring(lastIndex)));
  258. }
  259. return spans.isEmpty ? [TextSpan(text: text)] : spans;
  260. }
  261. TextSpan _buildAnnotationSpan(Annotation annotation) {
  262. switch (annotation.type) {
  263. case AnnotationType.highlight:
  264. // 高亮显示
  265. return TextSpan(
  266. text: annotation.text,
  267. style: TextStyle(
  268. backgroundColor: AppColors.primary.withOpacity(0.2),
  269. color: AppColors.primary,
  270. fontWeight: FontWeight.w500,
  271. ),
  272. );
  273. case AnnotationType.strikethrough:
  274. // 错别字:红色删除线 + 正确词提示
  275. return TextSpan(
  276. text: annotation.text,
  277. style: const TextStyle(
  278. color: AppColors.error,
  279. decoration: TextDecoration.lineThrough,
  280. decorationColor: AppColors.error,
  281. decorationThickness: 2,
  282. ),
  283. children: annotation.suggestion != null
  284. ? [
  285. const TextSpan(text: ' '),
  286. TextSpan(
  287. text: annotation.suggestion!,
  288. style: TextStyle(
  289. color: AppColors.success,
  290. backgroundColor: AppColors.success.withOpacity(0.1),
  291. fontWeight: FontWeight.w600,
  292. ),
  293. ),
  294. ]
  295. : null,
  296. );
  297. case AnnotationType.suggestion:
  298. // AI润色建议:浅蓝色背景
  299. return TextSpan(
  300. text: annotation.text,
  301. style: TextStyle(
  302. backgroundColor: AppColors.info.withOpacity(0.15),
  303. color: AppColors.info,
  304. fontStyle: FontStyle.italic,
  305. ),
  306. children: annotation.suggestion != null
  307. ? [
  308. const TextSpan(text: ' '),
  309. TextSpan(
  310. text: '💡 ${annotation.suggestion!}',
  311. style: TextStyle(
  312. fontSize: 12,
  313. color: AppColors.info,
  314. fontWeight: FontWeight.w500,
  315. backgroundColor: AppColors.info.withOpacity(0.2),
  316. ),
  317. ),
  318. ]
  319. : null,
  320. );
  321. }
  322. }
  323. Color _getElementColor(ElementType type) {
  324. switch (type) {
  325. case ElementType.amount:
  326. return AppColors.amount;
  327. case ElementType.company:
  328. return AppColors.company;
  329. case ElementType.person:
  330. return AppColors.person;
  331. case ElementType.location:
  332. return AppColors.location;
  333. case ElementType.date:
  334. return AppColors.date;
  335. case ElementType.other:
  336. return AppColors.other;
  337. }
  338. }
  339. }
  340. class _TextMarker {
  341. final int start;
  342. final int end;
  343. final _MarkerType type;
  344. final DocumentElement? element;
  345. final Annotation? annotation;
  346. _TextMarker({
  347. required this.start,
  348. required this.end,
  349. required this.type,
  350. this.element,
  351. this.annotation,
  352. });
  353. }
  354. enum _MarkerType {
  355. element,
  356. annotation,
  357. }