Browse Source

[new]谈话详情-谈话原文增加文本查找功能

zk 1 year ago
parent
commit
151de97422

BIN
assets/images/icon_talk_search.webp


BIN
assets/images/icon_talk_search_next.webp


BIN
assets/images/icon_talk_search_previous.webp


+ 2 - 0
assets/string/base/string.xml

@@ -179,4 +179,6 @@
     <string name="home_talk_see_more_txt">想找更多谈话?查看</string>
     <string name="home_talk_all">全部谈话</string>
     <string name="copy_success">已复制</string>
+    <string name="talk_detail_search_txt">查找</string>
+    <string name="talk_search_hint">输入关键词查找</string>
 </resources>

+ 0 - 1
lib/data/api/network_module.dart

@@ -3,7 +3,6 @@ import 'package:electronic_assistant/data/consts/build_config.dart';
 import 'package:electronic_assistant/utils/stream_dio_log_interceptor.dart';
 import 'package:pretty_dio_logger/pretty_dio_logger.dart';
 
-import '../consts/Constants.dart';
 
 class _NetworkModule {
   static Dio _createDefaultDio() {

+ 3 - 0
lib/data/bean/talk_original.dart

@@ -24,6 +24,9 @@ class TalkOriginal {
   String? sentence;
 
   @JsonKey(includeFromJson: false, includeToJson: false)
+  final Rxn<int> checkIndex = Rxn<int>();
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
   final Rxn<bool> _isSelected = Rxn<bool>();
 
   bool isSelected() => _isSelected.value ?? false;

+ 84 - 23
lib/module/talk/controller.dart

@@ -24,6 +24,7 @@ import 'package:electronic_assistant/utils/error_handler.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/utils/file_upload_check_helper.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
+import 'package:electronic_assistant/utils/pair.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
@@ -34,7 +35,6 @@ import 'package:get/get.dart';
 import 'package:just_audio/just_audio.dart';
 import 'package:share_plus/share_plus.dart';
 import 'package:wakelock_plus/wakelock_plus.dart';
-
 import '../../data/api/request/agenda_update_bean.dart';
 import '../../data/bean/agenda.dart';
 import '../../data/bean/agenda_list_all_bean.dart';
@@ -52,7 +52,6 @@ import '../../utils/event_bus.dart';
 import '../../utils/system_share_util.dart';
 import '../../utils/toast_util.dart';
 import 'package:webview_flutter/webview_flutter.dart';
-
 import 'mindmap/view.dart';
 import 'original/view.dart';
 
@@ -91,6 +90,8 @@ class TalkController extends BaseController {
 
   final _isEditModel = false.obs;
 
+  final isSearchModel = false.obs;
+
   final defaultIndex = 0;
 
   final Rxn<TalkBarBean> checkTabBean = Rxn();
@@ -143,13 +144,14 @@ class TalkController extends BaseController {
 
   final mindFullDuration = const Duration(milliseconds: 250);
 
+  GlobalKey tabBarGlobalKey = GlobalKey();
   GlobalKey headGlobalKey = GlobalKey();
   GlobalKey bottomGlobalKey = GlobalKey();
 
-  double? _bottomViewHeight;
-  final RxnDouble _headViewHeight = RxnDouble();
-
-  double? get headViewHeight => _headViewHeight.value;
+  //高度
+  RxnDouble bottomViewHeight = RxnDouble();
+  RxnDouble headViewHeight = RxnDouble();
+  RxnDouble tabBarHeight = RxnDouble();
 
   final DWebViewController webViewController =
       MindUtil.createMindWebViewController();
@@ -160,6 +162,12 @@ class TalkController extends BaseController {
 
   RxBool isShowMindMap = false.obs;
 
+  final RxString searchResultDesc = RxString('0/0');
+
+  final RxString searchPrint = RxString('');
+
+  Rxn<Pair<TalkBarType?, SearchOperationType>> searchOperationCallback = Rxn();
+
   @override
   void onInit() {
     super.onInit();
@@ -204,11 +212,14 @@ class TalkController extends BaseController {
         OriginalView(talkId),
       ]);
       tabBeans.assignAll([
-        TalkBarBean(TalkBarType.summary, StringName.talkTabSummary.tr, true),
-        TalkBarBean(TalkBarType.mindMap, StringName.talkMindMap.tr, false,
+        TalkBarBean(TalkBarType.summary, StringName.talkTabSummary.tr,
+            isShowEdit: true),
+        TalkBarBean(TalkBarType.mindMap, StringName.talkMindMap.tr,
             isDisallowScroll: true),
-        TalkBarBean(TalkBarType.myTask, StringName.talkTabMyTask.tr, true),
-        TalkBarBean(TalkBarType.original, StringName.talkTabOriginal.tr, false)
+        TalkBarBean(TalkBarType.myTask, StringName.talkTabMyTask.tr,
+            isShowEdit: true),
+        TalkBarBean(TalkBarType.original, StringName.talkTabOriginal.tr,
+            isShowSearch: true)
       ]);
       EventHandler.report(EventId.event_101401, params: {EventId.id: version});
     } else {
@@ -218,14 +229,23 @@ class TalkController extends BaseController {
         OriginalView(talkId),
       ]);
       tabBeans.assignAll([
-        TalkBarBean(TalkBarType.summary, StringName.talkTabSummary.tr, true),
-        TalkBarBean(TalkBarType.myTask, StringName.talkTabMyTask.tr, true),
-        TalkBarBean(TalkBarType.original, StringName.talkTabOriginal.tr, false)
+        TalkBarBean(TalkBarType.summary, StringName.talkTabSummary.tr,
+            isShowEdit: true),
+        TalkBarBean(TalkBarType.myTask, StringName.talkTabMyTask.tr,
+            isShowEdit: true),
+        TalkBarBean(TalkBarType.original, StringName.talkTabOriginal.tr,
+            isShowSearch: true)
       ]);
       EventHandler.report(EventId.event_101402, params: {EventId.id: version});
     }
     checkTabBean.value = tabBeans[defaultIndex];
     isInitializedView.value = true;
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      bottomViewHeight.value = bottomGlobalKey.currentContext?.size?.height;
+      headViewHeight.value = headGlobalKey.currentContext?.size?.height;
+      tabBarHeight.value = tabBarGlobalKey.currentContext?.size?.height;
+    });
   }
 
   @override
@@ -233,17 +253,25 @@ class TalkController extends BaseController {
     super.onReady();
     _initAudioPlayer();
     eventReport(EventId.event_101001, params: {EventId.id: eventTag});
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      _bottomViewHeight = bottomGlobalKey.currentContext?.size?.height;
-      _headViewHeight.value = headGlobalKey.currentContext?.size?.height;
-    });
   }
 
-  double getBottomViewHeight() {
-    if (_bottomViewHeight == null) {
-      return -250.h;
+  double? getChangeHeadHeight() {
+    if (_isEditModel.value == true || isSearchModel.value == true) {
+      return (headViewHeight.value ?? 0) - (tabBarHeight.value ?? 0);
     }
-    return -_bottomViewHeight!;
+    return isShowMindFullScreen.value ? 0 : headViewHeight.value;
+  }
+
+  double getChangeBottomHeight() {
+    return isShowMindFullScreen.value ||
+            _isEditModel.value ||
+            isSearchModel.value
+        ? getBottomViewHeight()
+        : 0.h;
+  }
+
+  double getBottomViewHeight() {
+    return bottomViewHeight.value != null ? -bottomViewHeight.value! : -250.h;
   }
 
   void eventReport(String eventId, {Map<String, dynamic>? params}) {
@@ -588,6 +616,10 @@ class TalkController extends BaseController {
     }
   }
 
+  void onSearchClick() {
+    isSearchModel.value = true;
+  }
+
   void updateTabIndex(int index) {
     checkTabBean.value = tabBeans[index];
   }
@@ -599,6 +631,12 @@ class TalkController extends BaseController {
         .toList());
   }
 
+  void onSearchCancel() {
+    isSearchModel.value = false;
+    searchPrint.value = '';
+    searchResultDesc.value = '0/0';
+  }
+
   void onEditDoneClick() {
     if (talkBean.value == null) {
       return;
@@ -907,6 +945,24 @@ class TalkController extends BaseController {
     });
   }
 
+  void setSearchChangeTxt(String value) {
+    searchPrint.value = value;
+  }
+
+  void updateSearchPositionDesc(int now, int total) {
+    searchResultDesc.value = '$now/$total';
+  }
+
+  void onSearchPrevious() {
+    searchOperationCallback.value =
+        Pair(checkTabBean.value?.type, SearchOperationType.previous);
+  }
+
+  void onSearchNext() {
+    searchOperationCallback.value =
+        Pair(checkTabBean.value?.type, SearchOperationType.next);
+  }
+
   @override
   void onClose() {
     super.onClose();
@@ -918,6 +974,8 @@ class TalkController extends BaseController {
   }
 }
 
+enum SearchOperationType { previous, next }
+
 enum TalkBarType { summary, mindMap, myTask, original }
 
 class TalkBarBean {
@@ -925,9 +983,12 @@ class TalkBarBean {
 
   final String title;
 
-  final bool isShowEdit;
+  final bool? isShowEdit;
 
   final bool? isDisallowScroll;
 
-  TalkBarBean(this.type, this.title, this.isShowEdit, {this.isDisallowScroll});
+  final bool? isShowSearch;
+
+  TalkBarBean(this.type, this.title,
+      {this.isShowEdit, this.isShowSearch, this.isDisallowScroll});
 }

+ 145 - 2
lib/module/talk/original/controller.dart

@@ -7,11 +7,13 @@ import 'package:electronic_assistant/module/talk/controller.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/utils/error_handler.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
-
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 import '../../../data/bean/talk_original.dart';
 import '../../../data/bean/talks.dart';
 import '../../../data/repositories/account_repository.dart';
+import '../../../widget/high_light_search_text.dart';
 
 class OriginalController extends BaseController {
   final originalList = <TalkOriginal>[].obs;
@@ -20,10 +22,23 @@ class OriginalController extends BaseController {
   StreamSubscription? _talkBeanListener;
   StreamSubscription? _talkStatusListener;
   StreamSubscription? _audioPlayingListener;
+  StreamSubscription? _searchPrintListener;
+  StreamSubscription? _searchOperationListener;
+
+  final Map<int, int> searchTotalMap = {};
 
   OriginalController(this.talkId);
 
-  get talkController => Get.find<TalkController>(tag: talkId);
+  TalkController get talkController => Get.find<TalkController>(tag: talkId);
+
+  RxString get searchPrint => talkController.searchPrint;
+
+  int searchNowIndex = 0;
+  int searchTotalSize = 0;
+
+  OriginalSearch? originalSearch;
+
+  final ItemScrollController itemScrollController = ItemScrollController();
 
   @override
   void onReady() {
@@ -39,10 +54,119 @@ class OriginalController extends BaseController {
         requestOriginal();
       }
     });
+
+    _searchPrintListener = talkController.searchPrint.listen((txt) {
+      if (talkController.checkTabBean.value?.type != TalkBarType.original) {
+        return;
+      }
+      _dealOriginalSearch(txt);
+    });
+
+    _searchOperationListener =
+        talkController.searchOperationCallback.listen((pair) {
+      if (pair == null) {
+        return;
+      }
+      if (pair.first != TalkBarType.original) {
+        return;
+      }
+      if (searchTotalMap.isEmpty) {
+        return;
+      }
+      if (pair.second == SearchOperationType.previous) {
+        _dealSearchOperation(searchNowIndex - 1);
+      } else if (pair.second == SearchOperationType.next) {
+        _dealSearchOperation(searchNowIndex + 1);
+      }
+    });
     requestOriginal();
     setPlayAutoSelection();
   }
 
+  void _dealSearchOperation(int updateIndex) {
+    if (searchTotalMap.isEmpty) {
+      originalSearch?.searchTargetBean?.checkIndex.value = null;
+      originalSearch = null;
+      searchNowIndex = 0;
+      searchTotalSize = 0;
+      talkController.updateSearchPositionDesc(searchNowIndex, searchTotalSize);
+      return;
+    }
+    if (originalSearch == null) {
+      _updateTargetBean(updateIndex, true);
+    } else {
+      updateSearchData(updateIndex);
+    }
+
+    scrollToIndex(originalSearch?.originalIndex);
+    talkController.updateSearchPositionDesc(searchNowIndex, searchTotalSize);
+  }
+
+  void updateSearchData(int updateIndex) {
+    if (updateIndex > searchNowIndex) {
+      int nextIndex = (originalSearch?.searchTargetIndex ?? 0) + 1;
+      if (nextIndex >= (originalSearch?.searchTargetLength ?? 0)) {
+        //找下一个
+        if (updateIndex > searchTotalSize) {
+          updateIndex = 1;
+        }
+        _updateTargetBean(updateIndex, true);
+      } else {
+        originalSearch?.searchTargetIndex = nextIndex;
+        originalSearch?.searchTargetBean?.checkIndex.value = nextIndex;
+      }
+    } else if (updateIndex < searchNowIndex) {
+      //上一个
+      int preIndex = (originalSearch?.searchTargetIndex ?? 0) - 1;
+      if (preIndex < 0) {
+        if (updateIndex <= 0) {
+          updateIndex = searchTotalSize;
+        }
+        _updateTargetBean(updateIndex, false);
+      } else {
+        originalSearch?.searchTargetIndex = preIndex;
+        originalSearch?.searchTargetBean?.checkIndex.value = preIndex;
+      }
+    }
+    searchNowIndex = updateIndex;
+  }
+
+  void _updateTargetBean(int updateIndex, bool isNextTarget) {
+    int count = 0;
+    for (var entry in searchTotalMap.entries) {
+      int key = entry.key;
+      int value = entry.value;
+      count += value;
+      if (updateIndex <= count) {
+        originalSearch ??= OriginalSearch();
+        originalSearch?.searchTargetIndex = isNextTarget ? 0 : value - 1;
+        originalSearch?.searchTargetLength = value;
+        originalSearch?.originalIndex = key;
+        originalSearch?.searchTargetBean?.checkIndex.value = null;
+        originalSearch?.searchTargetBean = originalList[key];
+        originalSearch?.searchTargetBean?.checkIndex.value =
+            originalSearch?.searchTargetIndex;
+        break;
+      }
+    }
+    searchNowIndex = updateIndex;
+  }
+
+  _dealOriginalSearch(String txt) {
+    searchTotalMap.clear();
+    searchTotalSize = 0;
+    for (int i = 0; i < originalList.length; i++) {
+      TalkOriginal item = originalList[i];
+      int count =
+          HighlightSearchText.getHighlightTotal(item.sentence ?? '', txt);
+      searchTotalSize += count;
+      if (count > 0) {
+        searchTotalMap.addAll({i: count});
+      }
+    }
+    _dealSearchOperation(searchTotalMap.isNotEmpty ? 1 : 0);
+  }
+
   void setPlayAutoSelection() {
     _audioPlayingListener = talkController.playingDuration.listen((duration) {
       if (duration == null) {
@@ -107,11 +231,30 @@ class OriginalController extends BaseController {
     });
   }
 
+  void scrollToIndex(int? index) {
+    if (index == null) {
+      return;
+    }
+    itemScrollController.scrollTo(
+        index: index,
+        duration: const Duration(milliseconds: 1),
+        curve: Curves.easeInOut);
+  }
+
   @override
   void onClose() {
     super.onClose();
     _talkBeanListener?.cancel();
     _talkStatusListener?.cancel();
     _audioPlayingListener?.cancel();
+    _searchPrintListener?.cancel();
+    _searchOperationListener?.cancel();
   }
 }
+
+class OriginalSearch {
+  TalkOriginal? searchTargetBean;
+  int? searchTargetIndex;
+  int? searchTargetLength;
+  int? originalIndex;
+}

+ 19 - 7
lib/module/talk/original/view.dart

@@ -7,8 +7,9 @@ import 'package:electronic_assistant/utils/expand.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
-
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 import '../../../data/bean/talk_original.dart';
+import '../../../widget/high_light_search_text.dart';
 import '../common_view.dart';
 import 'controller.dart';
 
@@ -55,7 +56,8 @@ class OriginalView extends BasePage<OriginalController> {
                         ?.toDouble() ??
                     0.0);
       } else {
-        return ListView.builder(
+        return ScrollablePositionedList.builder(
+          itemScrollController: controller.itemScrollController,
           padding: EdgeInsets.only(bottom: 150.h),
           itemBuilder: _buildOriginalItem,
           itemCount: controller.originalList.length,
@@ -124,12 +126,22 @@ class OriginalView extends BasePage<OriginalController> {
             ),
             SizedBox(height: 12.h),
             Obx(() {
-              return Text(item.sentence.toString(),
-                  style: TextStyle(
+              Color txtColor = item.isSelected()
+                  ? ColorName.colorPrimary
+                  : ColorName.primaryTextColor;
+              return HighlightSearchText(
+                  defaultHighlightIndex: item.checkIndex.value,
+                  normalTextStyle: TextStyle(fontSize: 14.sp, color: txtColor),
+                  highlightTextStyle: TextStyle(
+                      fontSize: 14.sp,
+                      color: txtColor,
+                      backgroundColor: '#ACE6FF'.color),
+                  activeHighlightTextStyle: TextStyle(
                       fontSize: 14.sp,
-                      color: item.isSelected()
-                          ? ColorName.colorPrimary
-                          : ColorName.primaryTextColor));
+                      color: txtColor,
+                      backgroundColor: '#FFE078'.color),
+                  text: item.sentence.toString(),
+                  searchKeyword: controller.searchPrint.value);
             }),
             Obx(() {
               return Visibility(

+ 13 - 11
lib/module/talk/summary/view.dart

@@ -9,7 +9,6 @@ import 'package:get/get.dart';
 import 'package:markdown/markdown.dart' as md;
 import '../../../data/bean/talks.dart';
 import '../../browser/view.dart';
-import '../../home/view.dart';
 import '../common_view.dart';
 import 'controller.dart';
 
@@ -124,16 +123,19 @@ class SummaryView extends BasePage<SummaryController> {
         return ListView(padding: EdgeInsets.only(bottom: 150.h), children: [
           SizedBox(height: 14.h),
           Obx(() {
-            return buildTemplateView(
-                controller.templateList, controller.templateSelectId,
-                onTap: (template) {
-              controller.selectTemplate(template);
-            },
-                addTemplateView: buildAddTemplateView(
-                    key: controller.addTemplateKey,
-                    addCallback: () {
-                      controller.addTemplateClick();
-                    }));
+            return Visibility(
+              visible: controller.isEditModel == false,
+              child: buildTemplateView(
+                  controller.templateList, controller.templateSelectId,
+                  onTap: (template) {
+                controller.selectTemplate(template);
+              },
+                  addTemplateView: buildAddTemplateView(
+                      key: controller.addTemplateKey,
+                      addCallback: () {
+                        controller.addTemplateClick();
+                      })),
+            );
           }),
           buildSummaryView(),
           Container(

+ 279 - 191
lib/module/talk/view.dart

@@ -63,6 +63,10 @@ class TalkPage extends BasePage<TalkController> {
           controller.onEditCancel();
           return false;
         }
+        if (controller.isSearchModel.value) {
+          controller.onSearchCancel();
+          return false;
+        }
         if (controller.isShowMindFullScreen.value) {
           controller.onExitMindFullScreen();
           return false;
@@ -97,7 +101,6 @@ class TalkPage extends BasePage<TalkController> {
         return Column(
           children: [
             AnimatedContainer(
-              key: controller.headGlobalKey,
               decoration: BoxDecoration(
                 gradient: LinearGradient(
                   colors: ['#E1E9FF'.toColor(), '#F9FAFE'.toColor()],
@@ -106,21 +109,13 @@ class TalkPage extends BasePage<TalkController> {
                   stops: const [0, 1.0],
                 ),
               ),
-              height: controller.isShowMindFullScreen.value
-                  ? 0
-                  : controller.headViewHeight,
+              height: controller.getChangeHeadHeight(),
               duration: controller.mindFullDuration,
               child: SingleChildScrollView(
                 physics: const NeverScrollableScrollPhysics(),
-                child: Column(children: [
+                child: Column(key: controller.headGlobalKey, children: [
                   SizedBox(height: statusBarHeight),
-                  Row(
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: [
-                      _buildToolbarLeftView(),
-                      _buildToolbarRightView(),
-                    ],
-                  ),
+                  _buildHeadView(),
                   buildTabBar(context),
                 ]),
               ),
@@ -132,16 +127,196 @@ class TalkPage extends BasePage<TalkController> {
     });
   }
 
+  Widget _buildHeadView() {
+    return Obx(() {
+      if (controller.isEditModel) {
+        return _buildEditHeadView();
+      }
+      if (controller.isSearchModel.value) {
+        return _buildSearchHeadView();
+      }
+      return _buildHeadNormalView();
+    });
+  }
+
+  Widget _buildSearchHeadView() {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: IconButton(
+            icon: Assets.images.iconTalkEditCancel
+                .image(width: 24.w, height: 24.w),
+            onPressed: () {
+              controller.onSearchCancel();
+            },
+          ),
+        ),
+        // TextField()
+        Expanded(
+            child: Container(
+          height: 38.w,
+          decoration: BoxDecoration(
+            color: ColorName.white,
+            border: Border.all(color: '#E7E9F6'.color, width: 1.w),
+            borderRadius: BorderRadius.circular(6.w),
+          ),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              Expanded(
+                child: TextField(
+                    maxLines: 1,
+                    style: TextStyle(
+                        fontSize: 14.sp, color: ColorName.primaryTextColor),
+                    // controller: controller.searchController,
+                    decoration: InputDecoration(
+                      hintText: StringName.talkSearchHint.tr,
+                      hintStyle: TextStyle(
+                        fontSize: 14.sp,
+                        color: ColorName.tertiaryTextColor,
+                      ),
+                      contentPadding: EdgeInsets.symmetric(horizontal: 8.w),
+                      border:
+                          const OutlineInputBorder(borderSide: BorderSide.none),
+                    ),
+                    onChanged: (value) {
+                      controller.setSearchChangeTxt(value);
+                    }),
+              ),
+              Text(controller.searchResultDesc.value,
+                  style: TextStyle(
+                      fontSize: 14.sp, color: ColorName.secondaryTextColor)),
+              SizedBox(width: 8.w)
+            ],
+          ),
+        )),
+        SizedBox(width: 11.w),
+        GestureDetector(
+            onTap: () {
+              controller.onSearchPrevious();
+            },
+            child: Assets.images.iconTalkSearchPrevious
+                .image(width: 24.w, height: 24.w)),
+        SizedBox(width: 16.w),
+        GestureDetector(
+            onTap: () {
+              controller.onSearchNext();
+            },
+            child: Assets.images.iconTalkSearchNext
+                .image(width: 24.w, height: 24.w)),
+        SizedBox(width: 12.w),
+      ],
+    );
+  }
+
+  Widget _buildEditHeadView() {
+    return Row(
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: IconButton(
+            icon: Assets.images.iconTalkEditCancel
+                .image(width: 24.w, height: 24.w),
+            onPressed: () {
+              controller.onEditCancel();
+            },
+          ),
+        ),
+        const Spacer(),
+        GestureDetector(
+          onTap: () {
+            controller.onEditDoneClick();
+          },
+          child: Padding(
+            padding: EdgeInsets.symmetric(horizontal: 12.w),
+            child: Text(StringName.done.tr,
+                style: TextStyle(
+                    fontSize: 17.sp, color: ColorName.primaryTextColor)),
+          ),
+        )
+      ],
+    );
+  }
+
+  Widget _buildHeadNormalView() {
+    return Row(
+      children: [
+        IconButton(
+          icon: Assets.images.iconTalkBack.image(width: 24.w, height: 24.w),
+          onPressed: () {
+            Get.back();
+          },
+        ),
+        SizedBox(width: 6.w),
+        Obx(() {
+          return GestureDetector(
+            onTap: () {
+              controller.onEditTitleClick();
+            },
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Row(
+                  children: [
+                    ConstrainedBox(
+                      constraints: BoxConstraints(maxWidth: 0.65.sw),
+                      child: Text(
+                        maxLines: 1,
+                        overflow: TextOverflow.ellipsis,
+                        controller.talkBean.value?.title.value ?? '',
+                        style: TextStyle(
+                          fontWeight: FontWeight.bold,
+                          fontSize: 15.sp,
+                          color: ColorName.primaryTextColor,
+                        ),
+                      ),
+                    ),
+                    SizedBox(width: 2.w),
+                    Assets.images.iconTalkEditTitle
+                        .image(width: 16.w, height: 16.w)
+                  ],
+                ),
+                SizedBox(height: 2.h),
+                Text(
+                  controller.talkBean.value?.createTime ?? '',
+                  style: TextStyle(
+                      fontSize: 11.sp, color: ColorName.secondaryTextColor),
+                )
+              ],
+            ),
+          );
+        }),
+        const Spacer(),
+        Row(
+          children: [
+            IconButton(
+              icon:
+                  Assets.images.iconTalkShare.image(width: 20.w, height: 20.w),
+              onPressed: () {
+                controller.onShareClick();
+              },
+            ),
+          ],
+        )
+      ],
+    );
+  }
+
   Widget buildTabBar(BuildContext context) {
     TabController tabController = DefaultTabController.of(context);
     tabController.addListener(() {
       controller.updateTabIndex(tabController.index);
     });
-    return Obx(() {
-      if (!controller.isEditModel) {
-        return Column(
-          children: [
-            TabBar(
+    return Column(
+      children: [
+        Obx(() {
+          return AbsorbPointer(
+            absorbing: controller.isEditModel || controller.isSearchModel.value,
+            child: TabBar(
+                key: controller.tabBarGlobalKey,
                 labelStyle:
                     TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
                 unselectedLabelStyle: TextStyle(fontSize: 14.sp),
@@ -158,14 +333,12 @@ class TalkPage extends BasePage<TalkController> {
                 tabs: controller.tabBeans
                     .map((bean) => Tab(text: bean.title))
                     .toList()),
-            SizedBox(height: 6.h),
-            Divider(height: 1, color: '#F2F4F9'.color)
-          ],
-        );
-      } else {
-        return SizedBox(height: 8.h, width: double.infinity);
-      }
-    });
+          );
+        }),
+        SizedBox(height: 6.h),
+        Divider(height: 1, color: '#F2F4F9'.color)
+      ],
+    );
   }
 
   @override
@@ -192,6 +365,7 @@ class TalkPage extends BasePage<TalkController> {
     return Expanded(
       child: TabBarView(
         physics: controller.isEditModel ||
+                controller.isSearchModel.value ||
                 controller.checkTabBean.value?.isDisallowScroll == true
             ? const NeverScrollableScrollPhysics()
             : null,
@@ -290,31 +464,26 @@ class TalkPage extends BasePage<TalkController> {
 
   Widget buildBottomView() {
     return Obx(() {
-      return Visibility(
+      return AnimatedPositioned(
         key: controller.bottomGlobalKey,
-        visible: controller.isEditModel == false,
-        child: AnimatedPositioned(
-          duration: controller.mindFullDuration,
-          bottom: controller.isShowMindFullScreen.value
-              ? controller.getBottomViewHeight()
-              : 0.h,
-          left: 0,
-          right: 0,
-          child: Align(
-            alignment: Alignment.bottomCenter,
-            child: Container(
-              margin: EdgeInsets.only(bottom: 20.h),
-              child: IntrinsicHeight(
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.end,
-                  children: [
-                    _buildAIAnalysisView(),
-                    SizedBox(height: 8.h),
-                    _buildTalkEditView(),
-                    SizedBox(height: 10.h),
-                    buildAudioView()
-                  ],
-                ),
+        duration: controller.mindFullDuration,
+        bottom: controller.getChangeBottomHeight(),
+        left: 0,
+        right: 0,
+        child: Align(
+          alignment: Alignment.bottomCenter,
+          child: Container(
+            margin: EdgeInsets.only(bottom: 20.h),
+            child: IntrinsicHeight(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.end,
+                children: [
+                  _buildAIAnalysisView(),
+                  SizedBox(height: 8.h),
+                  _buildOperationBtnView(),
+                  SizedBox(height: 10.h),
+                  buildAudioView()
+                ],
               ),
             ),
           ),
@@ -323,51 +492,70 @@ class TalkPage extends BasePage<TalkController> {
     });
   }
 
-  Widget _buildTalkEditView() {
-    return Obx(() {
-      return Visibility(
-        visible: controller.talkBean.value?.status.value ==
-                TalkStatus.analysisSuccess &&
-            controller.checkTabBean.value?.isShowEdit == true,
-        maintainState: true,
-        maintainAnimation: true,
-        maintainSize: true,
-        child: GestureDetector(
-          onTap: () => controller.onEditModelClick(),
-          child: Align(
-            alignment: Alignment.centerLeft,
-            child: Container(
-              margin: EdgeInsets.only(left: 12.w),
-              padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.w),
-              decoration: BoxDecoration(
-                  color: ColorName.white,
-                  borderRadius: BorderRadius.circular(100),
-                  boxShadow: [
-                    BoxShadow(
-                      color: ColorName.black5.withOpacity(0.08),
-                      spreadRadius: 2,
-                      blurRadius: 12,
-                      offset: const Offset(0, 1),
-                    ),
-                  ]),
-              child: IntrinsicWidth(
-                child: Row(
-                  children: [
-                    Assets.images.iconTalkEdit.image(width: 16.w, height: 16.w),
-                    SizedBox(width: 2.w),
-                    Text(StringName.talkUpdateTxt.tr,
-                        style: TextStyle(
-                            height: 1,
-                            fontSize: 14.sp,
-                            color: ColorName.primaryTextColor))
-                  ],
-                ),
+  Widget _buildOperationBtnView() {
+    return SizedBox(
+      height: 28.w,
+      child: Obx(() {
+        return Visibility(
+          visible: controller.talkBean.value?.status.value ==
+              TalkStatus.analysisSuccess,
+          child: Row(
+            children: [
+              Visibility(
+                  visible: controller.checkTabBean.value?.isShowEdit == true,
+                  child: _buildCommonOperationView(
+                      Assets.images.iconTalkEdit.provider(),
+                      StringName.talkUpdateTxt.tr, onClick: () {
+                    controller.onEditModelClick();
+                  })),
+              Visibility(
+                  visible: controller.checkTabBean.value?.isShowSearch == true,
+                  child: _buildCommonOperationView(
+                      Assets.images.iconTalkSearch.provider(),
+                      StringName.talkDetailSearchTxt.tr, onClick: () {
+                    controller.onSearchClick();
+                  })),
+            ],
+          ),
+        );
+      }),
+    );
+  }
+
+  Widget _buildCommonOperationView(ImageProvider provider, String txt,
+      {VoidCallback? onClick}) {
+    return GestureDetector(
+      onTap: () => onClick?.call(),
+      child: Container(
+        margin: EdgeInsets.only(left: 12.w),
+        height: 28.w,
+        padding: EdgeInsets.symmetric(horizontal: 8.w),
+        decoration: BoxDecoration(
+            color: ColorName.white,
+            borderRadius: BorderRadius.circular(100),
+            boxShadow: [
+              BoxShadow(
+                color: ColorName.black5.withOpacity(0.08),
+                spreadRadius: 2,
+                blurRadius: 12,
+                offset: const Offset(0, 1),
               ),
-            ),
+            ]),
+        child: IntrinsicWidth(
+          child: Row(
+            children: [
+              Image(image: provider, width: 16.w, height: 16.w),
+              SizedBox(width: 2.w),
+              Text(txt,
+                  style: TextStyle(
+                      height: 1,
+                      fontSize: 14.sp,
+                      color: ColorName.primaryTextColor))
+            ],
           ),
         ),
-      );
-    });
+      ),
+    );
   }
 
   Widget _buildAIAnalysisView() {
@@ -455,106 +643,6 @@ class TalkPage extends BasePage<TalkController> {
       ),
     );
   }
-
-  Widget _buildToolbarLeftView() {
-    return Obx(() {
-      if (controller.isEditModel) {
-        return Align(
-          alignment: Alignment.centerLeft,
-          child: IconButton(
-            icon: Assets.images.iconTalkEditCancel
-                .image(width: 24.w, height: 24.w),
-            onPressed: () {
-              controller.onEditCancel();
-            },
-          ),
-        );
-      } else {
-        return Row(
-          children: [
-            IconButton(
-              icon: Assets.images.iconTalkBack.image(width: 24.w, height: 24.w),
-              onPressed: () {
-                Get.back();
-              },
-            ),
-            SizedBox(width: 6.w),
-            Obx(() {
-              return GestureDetector(
-                onTap: () {
-                  controller.onEditTitleClick();
-                },
-                child: Column(
-                  mainAxisAlignment: MainAxisAlignment.center,
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Row(
-                      children: [
-                        ConstrainedBox(
-                          constraints: BoxConstraints(maxWidth: 0.65.sw),
-                          child: Text(
-                            maxLines: 1,
-                            overflow: TextOverflow.ellipsis,
-                            controller.talkBean.value?.title.value ?? '',
-                            style: TextStyle(
-                              fontWeight: FontWeight.bold,
-                              fontSize: 15.sp,
-                              color: ColorName.primaryTextColor,
-                            ),
-                          ),
-                        ),
-                        SizedBox(width: 2.w),
-                        Assets.images.iconTalkEditTitle
-                            .image(width: 16.w, height: 16.w)
-                      ],
-                    ),
-                    SizedBox(height: 2.h),
-                    Text(
-                      controller.talkBean.value?.createTime ?? '',
-                      style: TextStyle(
-                          fontSize: 11.sp, color: ColorName.secondaryTextColor),
-                    )
-                  ],
-                ),
-              );
-            })
-          ],
-        );
-      }
-    });
-  }
-
-  Widget _buildToolbarRightView() {
-    return Obx(() {
-      return Visibility(
-        visible: controller.talkBean.value?.status.value ==
-            TalkStatus.analysisSuccess,
-        child: controller.isEditModel
-            ? GestureDetector(
-                onTap: () {
-                  controller.onEditDoneClick();
-                },
-                child: Padding(
-                  padding: EdgeInsets.symmetric(horizontal: 12.w),
-                  child: Text(StringName.done.tr,
-                      style: TextStyle(
-                          fontSize: 17.sp, color: ColorName.primaryTextColor)),
-                ),
-              )
-            : Row(
-                children: [
-                  IconButton(
-                    icon: Assets.images.iconTalkShare
-                        .image(width: 20.w, height: 20.w),
-                    onPressed: () {
-                      controller.onShareClick();
-                    },
-                  ),
-                ],
-              ),
-      );
-    });
-  }
 }
 
 class CustomTrackShape extends RoundedRectSliderTrackShape {

+ 150 - 0
lib/widget/high_light_search_text.dart

@@ -0,0 +1,150 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class HighlightSearchText extends StatefulWidget {
+  final String text; // 原始文本
+  final String searchKeyword; // 搜索关键字
+  final int? defaultHighlightIndex; // 默认高亮的索引位置
+  final TextStyle normalTextStyle; // 普通文本样式
+  final TextStyle highlightTextStyle; // 高亮文本样式
+  final TextStyle activeHighlightTextStyle; // 定位高亮文本样式
+  final ValueChanged<Map<String, int>>? onHighlightChanged; // 高亮变化时的回调
+
+  const HighlightSearchText({
+    super.key,
+    required this.text,
+    required this.searchKeyword,
+    this.defaultHighlightIndex,
+    this.onHighlightChanged,
+    this.normalTextStyle = const TextStyle(color: Colors.black, fontSize: 16),
+    this.highlightTextStyle = const TextStyle(
+        color: Colors.white, backgroundColor: Colors.orange, fontSize: 16),
+    this.activeHighlightTextStyle = const TextStyle(
+        color: Colors.white, backgroundColor: Colors.red, fontSize: 16),
+  });
+
+  @override
+  State<HighlightSearchText> createState() => _HighlightSearchTextState();
+
+  static int getHighlightTotal(String targetTxt, String searchKeyword) {
+    if (searchKeyword.isEmpty || targetTxt.isEmpty) {
+      return 0;
+    }
+    int count = 0;
+    int start = 0;
+    while (true) {
+      int index = targetTxt.indexOf(searchKeyword, start);
+      if (index < 0) break;
+      count++;
+      start = index + searchKeyword.length;
+    }
+    return count;
+  }
+}
+
+class _HighlightSearchTextState extends State<HighlightSearchText> {
+  List<int> matchIndices = []; // 记录所有匹配的位置
+  int currentHighlightIndex = -1; // 当前高亮的索引
+
+  @override
+  void initState() {
+    super.initState();
+    currentHighlightIndex = widget.defaultHighlightIndex ?? -1;
+    _updateMatchIndices();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return RichText(
+      text: _buildHighlightedText(),
+    );
+  }
+
+  @override
+  void didUpdateWidget(covariant HighlightSearchText oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.text != oldWidget.text ||
+        widget.searchKeyword != oldWidget.searchKeyword ||
+        widget.defaultHighlightIndex != oldWidget.defaultHighlightIndex) {
+      currentHighlightIndex = widget.defaultHighlightIndex ?? -1;
+      // debugPrint(
+      //     'HighlightSearchText  currentHighlightIndex:$currentHighlightIndex '
+      //     'defaultHighlightIndex:${widget.defaultHighlightIndex} '
+      //     'matchIndices:${matchIndices.length} '
+      //     'text:${widget.text} '
+      //     ' searchKeyword:${widget.searchKeyword}');
+      _updateMatchIndices();
+      _notifyHighlightChanged();
+    }
+  }
+
+  void _notifyHighlightChanged() {
+    if (widget.onHighlightChanged != null) {
+      widget.onHighlightChanged!({
+        'current': currentHighlightIndex + 1, // 当前高亮位置,1-based
+        'total': matchIndices.length, // 总匹配数量
+      });
+    }
+  }
+
+  void _updateMatchIndices() {
+    matchIndices.clear();
+    if (widget.searchKeyword.isEmpty) return;
+
+    String textLower = widget.text.toLowerCase(); // 将原文本转换为小写
+    String keywordLower = widget.searchKeyword.toLowerCase(); // 将搜索关键字转换为小写
+
+    int start = 0;
+    while (true) {
+      int index = textLower.indexOf(keywordLower, start); // 在小写文本中进行匹配
+      if (index < 0) break;
+      matchIndices.add(index);
+      start = index + widget.searchKeyword.length;
+    }
+  }
+
+  TextSpan _buildHighlightedText() {
+    if (widget.searchKeyword.isEmpty) {
+      // 如果没有搜索关键词,返回原始文本
+      return TextSpan(
+        text: widget.text,
+        style: widget.normalTextStyle,
+      );
+    }
+
+    List<TextSpan> spans = [];
+    int start = 0;
+
+    for (int i = 0; i < matchIndices.length; i++) {
+      int index = matchIndices[i];
+
+      // 添加普通文本部分
+      if (index > start) {
+        spans.add(TextSpan(
+          text: widget.text.substring(start, index),
+          style: widget.normalTextStyle,
+        ));
+      }
+
+      // 添加高亮文本部分
+      spans.add(TextSpan(
+        text: widget.text.substring(index, index + widget.searchKeyword.length),
+        style: i == currentHighlightIndex
+            ? widget.activeHighlightTextStyle // 当前定位的高亮颜色
+            : widget.highlightTextStyle, // 普通高亮颜色
+      ));
+
+      start = index + widget.searchKeyword.length;
+    }
+
+    // 添加剩余普通文本部分
+    if (start < widget.text.length) {
+      spans.add(TextSpan(
+        text: widget.text.substring(start),
+        style: widget.normalTextStyle,
+      ));
+    }
+
+    return TextSpan(children: spans);
+  }
+}

+ 8 - 0
pubspec.lock

@@ -1286,6 +1286,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.28.0"
+  scrollable_positioned_list:
+    dependency: "direct main"
+    description:
+      name: scrollable_positioned_list
+      sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.8"
   share_plus:
     dependency: "direct main"
     description:

+ 3 - 0
pubspec.yaml

@@ -149,6 +149,9 @@ dependencies:
   #web通信
   dsbridge_flutter: ^1.1.0
 
+  #指定列表滚动到特定位置
+  scrollable_positioned_list: ^0.3.8
+
 dev_dependencies:
   flutter_test:
     sdk: flutter