| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_lucide/flutter_lucide.dart';
- import '../../models/element.dart';
- import '../../models/annotation.dart';
- import '../../theme/app_colors.dart';
- /// 增强的文本编辑器组件
- class EnhancedTextEditor extends StatefulWidget {
- final String text;
- final List<DocumentElement> elements;
- final List<Annotation> annotations;
- final Function(String selectedText)? onTextSelected;
- final Function(Annotation annotation)? onAnnotationAction;
- const EnhancedTextEditor({
- Key? key,
- required this.text,
- required this.elements,
- this.annotations = const [],
- this.onTextSelected,
- this.onAnnotationAction,
- }) : super(key: key);
- @override
- State<EnhancedTextEditor> createState() => _EnhancedTextEditorState();
- }
- class _EnhancedTextEditorState extends State<EnhancedTextEditor> {
- String? _selectedText;
- String _defaultSelectedText() {
- final t = widget.text.trim();
- if (t.isEmpty) return '';
- String firstLine = t.split('\n').first;
- // 切第一句
- if (firstLine.contains('。')) {
- firstLine = firstLine.split('。').first;
- }
- if (firstLine.length > 40) {
- firstLine = firstLine.substring(0, 40);
- }
- return firstLine;
- }
- @override
- Widget build(BuildContext context) {
- final String displaySelected =
- (_selectedText != null && _selectedText!.isNotEmpty)
- ? _selectedText!
- : _defaultSelectedText();
- return Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // 固定显示的操作工具栏(无选择时使用默认文本)
- Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- decoration: BoxDecoration(
- color: AppColors.primary.withOpacity(0.05),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: AppColors.primary.withOpacity(0.2)),
- ),
- child: Row(
- children: [
- Expanded(
- child: Text(
- '选中: "$displaySelected"',
- style: TextStyle(
- fontSize: 13,
- color: AppColors.textPrimary,
- fontWeight: FontWeight.w500,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- const SizedBox(width: 8),
- _buildToolbarButton(
- icon: LucideIcons.highlighter,
- label: '标记为要素',
- onTap: () {
- if (displaySelected.isNotEmpty) {
- widget.onTextSelected?.call(displaySelected);
- }
- },
- ),
- const SizedBox(width: 4),
- _buildToolbarButton(
- icon: LucideIcons.spell_check,
- label: '检查拼写',
- onTap: () {},
- ),
- const SizedBox(width: 4),
- _buildToolbarButton(
- icon: LucideIcons.sparkles,
- label: 'AI润色',
- onTap: () {},
- ),
- const SizedBox(width: 4),
- _buildToolbarButton(
- icon: LucideIcons.copy,
- label: '复制',
- onTap: () {
- if (displaySelected.isNotEmpty) {
- Clipboard.setData(ClipboardData(text: displaySelected));
- }
- },
- ),
- ],
- ),
- ),
- // 文本内容
- GestureDetector(
- onTap: () {},
- child: SingleChildScrollView(
- child: SelectableText.rich(
- TextSpan(
- children: _buildTextSpans(),
- style: const TextStyle(
- fontSize: 15,
- height: 1.8,
- color: AppColors.textPrimary,
- ),
- ),
- onSelectionChanged: (selection, cause) {
- if (selection.isValid && !selection.isCollapsed) {
- final text = widget.text.substring(
- selection.start,
- selection.end,
- );
- setState(() {
- _selectedText = text;
- });
- }
- },
- ),
- ),
- ),
- ],
- );
- }
- Widget _buildToolbarButton({
- required IconData icon,
- required String label,
- required VoidCallback onTap,
- }) {
- return Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(6),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(icon, size: 14, color: AppColors.primary),
- const SizedBox(width: 4),
- Text(
- label,
- style: TextStyle(
- fontSize: 12,
- color: AppColors.primary,
- fontWeight: FontWeight.w500,
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
- List<TextSpan> _buildTextSpans() {
- final spans = <TextSpan>[];
- final text = widget.text;
- int lastIndex = 0;
- // 合并要素和批注,按位置排序
- final allMarkers = <_TextMarker>[];
- // 添加要素标记
- for (final element in widget.elements) {
- final index = text.indexOf(element.value, lastIndex);
- if (index != -1) {
- allMarkers.add(_TextMarker(
- start: index,
- end: index + element.value.length,
- type: _MarkerType.element,
- element: element,
- ));
- }
- }
- // 添加批注标记
- for (final annotation in widget.annotations) {
- final index = text.indexOf(annotation.text);
- if (index != -1) {
- allMarkers.add(_TextMarker(
- start: index,
- end: index + annotation.text.length,
- type: _MarkerType.annotation,
- annotation: annotation,
- ));
- }
- }
- // 按位置排序
- allMarkers.sort((a, b) => a.start.compareTo(b.start));
- // 构建文本片段
- for (final marker in allMarkers) {
- // 添加标记前的文本
- if (marker.start > lastIndex) {
- spans.add(TextSpan(text: text.substring(lastIndex, marker.start)));
- }
- // 添加标记文本
- if (marker.type == _MarkerType.element) {
- final element = marker.element!;
- final color = _getElementColor(element.type);
- spans.add(
- TextSpan(
- children: [
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: Container(
- margin: const EdgeInsets.only(right: 4),
- padding:
- const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: color.withOpacity(0.15),
- borderRadius: BorderRadius.circular(4),
- border: Border.all(color: color.withOpacity(0.4)),
- ),
- child: Text(
- element.label,
- style: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.w600,
- color: color,
- ),
- ),
- ),
- ),
- TextSpan(
- text: element.value,
- style: TextStyle(
- backgroundColor: color.withOpacity(0.2),
- color: color,
- fontWeight: FontWeight.w600,
- decoration: TextDecoration.underline,
- decorationColor: color,
- ),
- mouseCursor: SystemMouseCursors.click,
- ),
- ],
- ),
- );
- } else if (marker.type == _MarkerType.annotation) {
- final annotation = marker.annotation!;
- spans.add(_buildAnnotationSpan(annotation));
- }
- lastIndex = marker.end;
- }
- // 添加剩余文本
- if (lastIndex < text.length) {
- spans.add(TextSpan(text: text.substring(lastIndex)));
- }
- return spans.isEmpty ? [TextSpan(text: text)] : spans;
- }
- TextSpan _buildAnnotationSpan(Annotation annotation) {
- switch (annotation.type) {
- case AnnotationType.highlight:
- // 高亮显示
- return TextSpan(
- text: annotation.text,
- style: TextStyle(
- backgroundColor: AppColors.primary.withOpacity(0.2),
- color: AppColors.primary,
- fontWeight: FontWeight.w500,
- ),
- );
- case AnnotationType.strikethrough:
- // 错别字:红色删除线 + 正确词提示
- return TextSpan(
- text: annotation.text,
- style: const TextStyle(
- color: AppColors.error,
- decoration: TextDecoration.lineThrough,
- decorationColor: AppColors.error,
- decorationThickness: 2,
- ),
- children: annotation.suggestion != null
- ? [
- const TextSpan(text: ' '),
- TextSpan(
- text: annotation.suggestion!,
- style: TextStyle(
- color: AppColors.success,
- backgroundColor: AppColors.success.withOpacity(0.1),
- fontWeight: FontWeight.w600,
- ),
- ),
- ]
- : null,
- );
- case AnnotationType.suggestion:
- // AI润色建议:浅蓝色背景
- return TextSpan(
- text: annotation.text,
- style: TextStyle(
- backgroundColor: AppColors.info.withOpacity(0.15),
- color: AppColors.info,
- fontStyle: FontStyle.italic,
- ),
- children: annotation.suggestion != null
- ? [
- const TextSpan(text: ' '),
- TextSpan(
- text: '💡 ${annotation.suggestion!}',
- style: TextStyle(
- fontSize: 12,
- color: AppColors.info,
- fontWeight: FontWeight.w500,
- backgroundColor: AppColors.info.withOpacity(0.2),
- ),
- ),
- ]
- : null,
- );
- }
- }
- Color _getElementColor(ElementType type) {
- switch (type) {
- case ElementType.amount:
- return AppColors.amount;
- case ElementType.company:
- return AppColors.company;
- case ElementType.person:
- return AppColors.person;
- case ElementType.location:
- return AppColors.location;
- case ElementType.date:
- return AppColors.date;
- case ElementType.other:
- return AppColors.other;
- }
- }
- }
- class _TextMarker {
- final int start;
- final int end;
- final _MarkerType type;
- final DocumentElement? element;
- final Annotation? annotation;
- _TextMarker({
- required this.start,
- required this.end,
- required this.type,
- this.element,
- this.annotation,
- });
- }
- enum _MarkerType {
- element,
- annotation,
- }
|