搜索界面使用Flutter自带的SearchDelegate组件实现,通过魔改实现如下效果:

  1. 搜素建议
  2. 搜索结果,支持刷新和加载更多
  3. IOS中文输入拼音问题

界面预览


拷贝源码

将SearchDelegate的源码拷贝一份,修改内容如下:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; /// 修改此处为 showMySearch
Future<T?> showMySearch<T>({
required BuildContext context,
required MySearchDelegate<T> delegate,
String? query = '',
bool useRootNavigator = false,
}) {
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
return Navigator.of(context, rootNavigator: useRootNavigator)
.push(_SearchPageRoute<T>(
delegate: delegate,
));
} /// https://juejin.cn/post/7090374603951833118
abstract class MySearchDelegate<T> {
MySearchDelegate({
this.searchFieldLabel,
this.searchFieldStyle,
this.searchFieldDecorationTheme,
this.keyboardType,
this.textInputAction = TextInputAction.search,
}) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null); Widget buildSuggestions(BuildContext context); Widget buildResults(BuildContext context); Widget? buildLeading(BuildContext context); bool? automaticallyImplyLeading; double? leadingWidth; List<Widget>? buildActions(BuildContext context); PreferredSizeWidget? buildBottom(BuildContext context) => null; Widget? buildFlexibleSpace(BuildContext context) => null; ThemeData appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith(
appBarTheme: AppBarTheme(
systemOverlayStyle: colorScheme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
backgroundColor: colorScheme.brightness == Brightness.dark
? Colors.grey[900]
: Colors.white,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
titleTextStyle: theme.textTheme.titleLarge,
toolbarTextStyle: theme.textTheme.bodyMedium,
),
inputDecorationTheme: searchFieldDecorationTheme ??
InputDecorationTheme(
hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle,
border: InputBorder.none,
),
);
} String get query => _queryTextController.completeText; set query(String value) {
_queryTextController.completeText = value; // 更新实际搜索内容
_queryTextController.text = value; // 更新输入框内容
if (_queryTextController.text.isNotEmpty) {
_queryTextController.selection = TextSelection.fromPosition(
TextPosition(offset: _queryTextController.text.length));
}
} void showResults(BuildContext context) {
_focusNode?.unfocus();
_currentBody = _SearchBody.results;
} void showSuggestions(BuildContext context) {
assert(_focusNode != null,
'_focusNode must be set by route before showSuggestions is called.');
_focusNode!.requestFocus();
_currentBody = _SearchBody.suggestions;
} void close(BuildContext context, T result) {
_currentBody = null;
_focusNode?.unfocus();
Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route)
..pop(result);
} final String? searchFieldLabel; final TextStyle? searchFieldStyle; final InputDecorationTheme? searchFieldDecorationTheme; final TextInputType? keyboardType; final TextInputAction textInputAction; Animation<double> get transitionAnimation => _proxyAnimation; FocusNode? _focusNode; final ChinaTextEditController _queryTextController = ChinaTextEditController(); final ProxyAnimation _proxyAnimation =
ProxyAnimation(kAlwaysDismissedAnimation); final ValueNotifier<_SearchBody?> _currentBodyNotifier =
ValueNotifier<_SearchBody?>(null); _SearchBody? get _currentBody => _currentBodyNotifier.value;
set _currentBody(_SearchBody? value) {
_currentBodyNotifier.value = value;
} _SearchPageRoute<T>? _route; /// Releases the resources.
@mustCallSuper
void dispose() {
_currentBodyNotifier.dispose();
_focusNode?.dispose();
_queryTextController.dispose();
_proxyAnimation.parent = null;
}
} /// search page.
enum _SearchBody {
suggestions, results,
} class _SearchPageRoute<T> extends PageRoute<T> {
_SearchPageRoute({
required this.delegate,
}) {
assert(
delegate._route == null,
'The ${delegate.runtimeType} instance is currently used by another active '
'search. Please close that search by calling close() on the MySearchDelegate '
'before opening another search with the same delegate instance.',
);
delegate._route = this;
} final MySearchDelegate<T> delegate; @override
Color? get barrierColor => null; @override
String? get barrierLabel => null; @override
Duration get transitionDuration => const Duration(milliseconds: 300); @override
bool get maintainState => false; @override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: animation,
child: child,
);
} @override
Animation<double> createAnimation() {
final Animation<double> animation = super.createAnimation();
delegate._proxyAnimation.parent = animation;
return animation;
} @override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return _SearchPage<T>(
delegate: delegate,
animation: animation,
);
} @override
void didComplete(T? result) {
super.didComplete(result);
assert(delegate._route == this);
delegate._route = null;
delegate._currentBody = null;
}
} class _SearchPage<T> extends StatefulWidget {
const _SearchPage({
required this.delegate,
required this.animation,
}); final MySearchDelegate<T> delegate;
final Animation<double> animation; @override
State<StatefulWidget> createState() => _SearchPageState<T>();
} class _SearchPageState<T> extends State<_SearchPage<T>> {
// This node is owned, but not hosted by, the search page. Hosting is done by
// the text field.
FocusNode focusNode = FocusNode(); @override
void initState() {
super.initState();
widget.delegate._queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
} @override
void dispose() {
super.dispose();
widget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode = null;
focusNode.dispose();
} void _onAnimationStatusChanged(AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) {
focusNode.requestFocus();
}
} @override
void didUpdateWidget(_SearchPage<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.delegate != oldWidget.delegate) {
oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.delegate._queryTextController.addListener(_onQueryChanged);
oldWidget.delegate._currentBodyNotifier
.removeListener(_onSearchBodyChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
oldWidget.delegate._focusNode = null;
widget.delegate._focusNode = focusNode;
}
} void _onFocusChanged() {
if (focusNode.hasFocus &&
widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
}
} void _onQueryChanged() {
setState(() {
// rebuild ourselves because query changed.
});
} void _onSearchBodyChanged() {
setState(() {
// rebuild ourselves because search body changed.
});
} @override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = widget.delegate.appBarTheme(context);
final String searchFieldLabel = widget.delegate.searchFieldLabel ??
MaterialLocalizations.of(context).searchFieldLabel;
Widget? body;
switch (widget.delegate._currentBody) {
case _SearchBody.suggestions:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
case _SearchBody.results:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.results),
child: widget.delegate.buildResults(context),
);
case null:
break;
} late final String routeName;
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
routeName = '';
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
routeName = searchFieldLabel;
} return Semantics(
explicitChildNodes: true,
scopesRoute: true,
namesRoute: true,
label: routeName,
child: Theme(
data: theme,
child: Scaffold(
appBar: AppBar(
leadingWidth: widget.delegate.leadingWidth,
automaticallyImplyLeading:
widget.delegate.automaticallyImplyLeading ?? true,
leading: widget.delegate.buildLeading(context),
title: TextField(
controller: widget.delegate._queryTextController,
focusNode: focusNode,
style: widget.delegate.searchFieldStyle ??
theme.textTheme.titleLarge,
textInputAction: widget.delegate.textInputAction,
keyboardType: widget.delegate.keyboardType,
onSubmitted: (String _) => widget.delegate.showResults(context),
decoration: InputDecoration(hintText: searchFieldLabel),
),
flexibleSpace: widget.delegate.buildFlexibleSpace(context),
actions: widget.delegate.buildActions(context),
bottom: widget.delegate.buildBottom(context),
),
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
),
),
);
}
} class ChinaTextEditController extends TextEditingController {
///拼音输入完成后的文字
var completeText = ''; @override
TextSpan buildTextSpan(
{required BuildContext context,
TextStyle? style,
required bool withComposing}) {
///拼音输入完成
if (!value.composing.isValid || !withComposing) {
if (completeText != value.text) {
completeText = value.text;
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
}
return TextSpan(style: style, text: text);
} ///返回输入样式,可自定义样式
final TextStyle composingStyle = style?.merge(
const TextStyle(decoration: TextDecoration.underline),
) ?? const TextStyle(decoration: TextDecoration.underline);
return TextSpan(style: style, children: <TextSpan>[
TextSpan(text: value.composing.textBefore(value.text)),
TextSpan(
style: composingStyle,
text: value.composing.isValid && !value.composing.isCollapsed
? value.composing.textInside(value.text)
: "",
),
TextSpan(text: value.composing.textAfter(value.text)),
]);
}
}

实现搜索

创建SearchPage继承MySearchDelegate,修改样式,实现页面。需要重写下面5个方法

  • appBarTheme:修改搜索样式
  • buildActions:搜索框右侧的方法
  • buildLeading:搜索框左侧的返回按钮
  • buildResults:搜索结果
  • buildSuggestions:搜索建议
import 'package:e_book_clone/pages/search/MySearchDelegate.dart';
import 'package:flutter/src/material/theme_data.dart';
import 'package:flutter/src/widgets/framework.dart'; class Demo extends MySearchDelegate { @override
ThemeData appBarTheme(BuildContext context) {
// TODO: implement appBarTheme
return super.appBarTheme(context);
} @override
List<Widget>? buildActions(BuildContext context) {
// TODO: implement buildActions
throw UnimplementedError();
} @override
Widget? buildLeading(BuildContext context) {
// TODO: implement buildLeading
throw UnimplementedError();
} @override
Widget buildResults(BuildContext context) {
// TODO: implement buildResults
throw UnimplementedError();
} @override
Widget buildSuggestions(BuildContext context) {
// TODO: implement buildSuggestions
throw UnimplementedError();
}
}

修改样式

@override
ThemeData appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith( // 使用copyWith,适配全局主题
appBarTheme: AppBarTheme( // AppBar样式修改
systemOverlayStyle: colorScheme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
surfaceTintColor: Theme.of(context).colorScheme.surface,
titleSpacing: 0, // textfield前面的间距
elevation: 0, // 阴影
),
inputDecorationTheme: InputDecorationTheme(
isCollapsed: true,
hintStyle: TextStyle( // 提示文字颜色
color: Theme.of(ToastUtils.context).colorScheme.inversePrimary),
filled: true, // 填充颜色
contentPadding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 15.w),
fillColor: Theme.of(context).colorScheme.secondary, // 填充颜色,需要配合 filled
enabledBorder: OutlineInputBorder( // testified 边框
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surface,
),
),
focusedBorder: OutlineInputBorder( // testified 边框
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surface,
),
),
),
);
} @override
TextStyle? get searchFieldStyle => TextStyle(fontSize: 14.sp); // 字体大小设置,主要是覆盖默认样式

按钮功能

左侧返回按钮,右侧就放了一个搜索文本,点击之后显示搜索结果

@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: Icon(
color: Theme.of(context).colorScheme.onSurface,
Icons.arrow_back_ios_new,
size: 20.r,
),
);
} @override
List<Widget>? buildActions(BuildContext context) {
return [
Padding(
padding: EdgeInsets.only(right: 15.w, left: 15.w),
child: GestureDetector(
onTap: () {
showResults(context);
},
child: Text(
'搜索',
style: TextStyle(
color: Theme.of(context).colorScheme.primary, fontSize: 15.sp),
),
),
)
];
}

搜索建议

当 TextField 输入变化时,就会调用buildSuggestions方法,刷新布局,因此考虑使用FlutterBuilder管理页面和数据。

final SearchViewModel _viewModel = SearchViewModel();

@override
Widget buildSuggestions(BuildContext context) {
if (query.isEmpty) {
// 这里可以展示热门搜索等,有搜索建议时,热门搜索会被替换成搜索建议
return const SizedBox();
}
return FutureBuilder(
future: _viewModel.getSuggest(query),
builder: (BuildContext context, AsyncSnapshot<List<Suggest>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// 数据加载中
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// 数据加载错误
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
// 数据加载成功,展示结果
final List<Suggest> searchResults = snapshot.data ?? [];
return ListView.builder(
padding: EdgeInsets.all(15.r),
itemCount: searchResults.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 更新输入框
query = searchResults[index].text ?? query;
showResults(context);
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.h),
decoration: BoxDecoration(
border: BorderDirectional(
bottom: BorderSide(
width: 0.6,
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
),
child: Text('${searchResults[index].text}'),
),
);
});
} else {
// 数据为空
return const Center(child: Text('No results found'));
}
},
);
}

实体类代码如下:

class Suggest {
Suggest({
this.id,
this.url,
this.text,
this.isHot,
this.hotLevel,
}); Suggest.fromJson(dynamic json) {
id = json['id'];
url = json['url'];
text = json['text'];
isHot = json['is_hot'];
hotLevel = json['hot_level'];
} String? id;
String? url;
String? text;
bool? isHot;
int? hotLevel;
}

ViewModel代码如下:

class SearchViewModel {
Future<List<Suggest>> getSuggest(String keyword) async {
if (keyword.isEmpty) {
return [];
}
return await JsonApi.instance().fetchSuggestV3(keyword);
}
}

搜索结果

我们需要搜索结果页面支持加载更多,这里用到了 SmartRefrsh 组件

flutter pub add pull_to_refresh

buildResults方法是通过调用showResults(context);方法刷新页面,因此为了方便数据动态变化,新建search_result_page.dart页面

import 'package:e_book_clone/components/book_tile/book_tile_vertical/my_book_tile_vertical_item.dart';
import 'package:e_book_clone/components/book_tile/book_tile_vertical/my_book_tile_vertical_item_skeleton.dart';
import 'package:e_book_clone/components/my_smart_refresh.dart';
import 'package:e_book_clone/models/book.dart';
import 'package:e_book_clone/models/types.dart';
import 'package:e_book_clone/pages/search/search_vm.dart';
import 'package:e_book_clone/utils/navigator_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; class SearchResultPage extends StatefulWidget {
final String query; // 请求参数
const SearchResultPage({super.key, required this.query}); @override
State<SearchResultPage> createState() => _SearchResultPageState();
} class _SearchResultPageState extends State<SearchResultPage> {
final RefreshController _refreshController = RefreshController();
final SearchViewModel _viewModel = SearchViewModel();
void loadOrRefresh(bool loadMore) {
_viewModel.getResults(widget.query, loadMore).then((_) {
if (loadMore) {
_refreshController.loadComplete();
} else {
_refreshController.refreshCompleted();
}
});
} @override
void initState() {
super.initState();
loadOrRefresh(false);
} @override
void dispose() {
_viewModel.isDispose = true;
_refreshController.dispose();
super.dispose();
} @override
Widget build(BuildContext context) {
return ChangeNotifierProvider<SearchViewModel>.value(
value: _viewModel,
builder: (context, child) {
return Consumer<SearchViewModel>(
builder: (context, vm, child) {
List<Book>? searchResult = vm.searchResult;
// 下拉刷新和上拉加载组件
return MySmartRefresh(
enablePullDown: false,
onLoading: () {
loadOrRefresh(true);
},
controller: _refreshController,
child: ListView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w, top: 15.h),
itemCount: searchResult?.length ?? 10,
itemBuilder: (context, index) {
if (searchResult == null) {
// 骨架屏
return MyBookTileVerticalItemSkeleton(
width: 80.w, height: 120.h);
}
// 结果渲染组件
return MyBookTileVerticalItem(
book: searchResult[index],
width: 80.w,
height: 120.h,
onTap: (id) {
NavigatorUtils.nav2Detail(
context, DetailPageType.ebook, searchResult[index]);
},
);
},
),
);
},
);
},
);
}
}

MySmartRefresh组件代码如下,主要是对SmartRefresher做了进一步的封装

import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; class MySmartRefresh extends StatelessWidget {
// 启用下拉
final bool? enablePullDown; // 启用上拉
final bool? enablePullUp; // 头布局
final Widget? header; // 尾布局
final Widget? footer; // 刷新事件
final VoidCallback? onRefresh; // 加载事件
final VoidCallback? onLoading; // 刷新组件控制器
final RefreshController controller; final ScrollController? scrollController; // 被刷新的子组件
final Widget child; const MySmartRefresh({
super.key,
this.enablePullDown,
this.enablePullUp,
this.header,
this.footer,
this.onLoading,
this.onRefresh,
required this.controller,
required this.child,
this.scrollController,
}); @override
Widget build(BuildContext context) {
return _refreshView();
} Widget _refreshView() {
return SmartRefresher(
scrollController: scrollController,
controller: controller,
enablePullDown: enablePullDown ?? true,
enablePullUp: enablePullUp ?? true,
header: header ?? const ClassicHeader(),
footer: footer ?? const ClassicFooter(),
onRefresh: onRefresh,
onLoading: onLoading,
child: child,
);
}
}

SearchViewModel 代码如下:

import 'package:e_book_clone/http/spider/json_api.dart';
import 'package:e_book_clone/models/book.dart';
import 'package:e_book_clone/models/query_param.dart';
import 'package:e_book_clone/models/suggest.dart';
import 'package:flutter/material.dart'; class SearchViewModel extends ChangeNotifier {
int _currPage = 2;
bool isDispose = false;
List<Book>? _searchResult; List<Book>? get searchResult => _searchResult; Future<List<Suggest>> getSuggest(String keyword) async {
if (keyword.isEmpty) {
return [];
}
return await JsonApi.instance().fetchSuggestV3(keyword);
} Future getResults(String keyword, bool loadMore, {VoidCallback? callback}) async {
if (loadMore) {
_currPage++;
} else {
_currPage = 1;
_searchResult?.clear();
} // 请求参数
SearchParam param = SearchParam(
page: _currPage,
rootKind: null,
q: keyword,
sort: "defalut",
query: SearchParam.ebookSearch,
);
// 请求结果
List<Book> res = await JsonApi.instance().fetchEbookSearch(param); // 加载更多,使用addAll
if (_searchResult == null) {
_searchResult = res;
} else {
_searchResult!.addAll(res);
} if (res.isEmpty && _currPage > 0) {
_currPage--;
}
// 防止Provider被销毁,数据延迟请求去通知报错
if (isDispose) return;
notifyListeners();
}
}

buildResults方法如下:

@override
Widget buildResults(BuildContext context) {
if (query.isEmpty) {
return const SizedBox();
}
return SearchResultPage(query: query);
}

显示搜索界面

注意调用的是我们自己拷贝修改的MySearchDelegate中的方法

onTap: () {
showMySearch(context: context, delegate: SearchPage());
},

更多内容见

Flutter 借助SearchDelegate实现搜索页面,实现搜索建议、搜索结果,解决IOS拼音问题的更多相关文章

  1. 【音乐App】—— Vue-music 项目学习笔记:搜索页面开发

    前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 搜索歌手歌曲 搜索历史保存 ...

  2. Flutter: SearchDelegate 委托showSearch定义搜索页面的内容

    API class _MyHomeState extends State<MyHome> { List<String> _list = List.generate(100, ( ...

  3. destoon公司搜索页面显示公司类型

    首先找到前台模板文件:/template/default/company/search.htm 看到51行 {template 'list-company', 'tag'} 打开 /template/ ...

  4. tp5页面输出时,搜索后跳转下一页的处理

    tp5页面输出时,搜索功能在跳转下一页时,如果不做任何处理,会返回原有是第二页输出的数据.为了保证跳转下一页时输出的是搜索到的数据,做以下处理. (要根据自己的搜索字段进行适当修改) 页面js代码,给 ...

  5. 解决dede搜索页面只能显示10条信息解决方案

    解决dede搜索页面只能显示10条信息解决方案,感觉显示的信息太少,这时就要想办法去解决一下.看看有什么好办法来解决一下这个问题. dede搜索页模板中,默认只能显示10条记录. 打开dede搜索页模 ...

  6. Umbraco中更换IndexSet中的NodeType后,搜索页面没有做出对应更改的效果

    在项目开发中,使用ExternalSearcher,有一个ExamineIndex.config文件中存放ExternalIndexSet 开始时是这样的 <!-- Default Indexs ...

  7. 学习用java基于webMagic+selenium+phantomjs实现爬虫Demo爬取淘宝搜索页面

    由于业务需要,老大要我研究一下爬虫. 团队的技术栈以java为主,并且我的主语言是Java,研究时间不到一周.基于以上原因固放弃python,选择java为语言来进行开发.等之后有时间再尝试pytho ...

  8. dedecms站内搜索页面调用最新文章

    在页面中调用最新文章列表可以使新发布的文章更快被收录,如何在dedecms站内搜索页面调用最新文章呢? 1.登陆系统后台,进入“模板——模板管理——自定义宏标记”,点击“智能标记向导”进入智能标记生成 ...

  9. Vue音乐项目笔记(四)(搜索页面提取重写)

    1.如何通过betterScroll组件实现上拉刷新 https://blog.csdn.net/weixin_40814356/article/details/80478440 2.搜索页面跳转单曲 ...

  10. 如何根据搜索页面内容得到的结果生成该元素的xpath路径

    如何根据搜索页面内容得到的结果生成该元素的xpath路径?

随机推荐

  1. [FE] 实时视频流库 hls.js 重载切换资源的方式

    hls 播放需要先 attachMedia,然后 loadSource. 如果切换 resource,需要先执行 hls.destroy(),否则会出现混乱. destroy 之后再依次进行 hls ...

  2. WPF 自己封装 Skia 差量绘制控件

    使用 Skia 能做到在多个不同的平台使用相同的一套 API 绘制出相同界面效果的图片,可以将图片绘制到应用程序的渲染显示里面.在 WPF 中最稳的方法就是通过 WriteableBitmap 作为承 ...

  3. WPF 让窗口激活作为前台最上层窗口的方法

    在 WPF 中,如果想要使用代码控制,让某个窗口作为当前用户的输入的逻辑焦点的窗口,也就是在当前用户活动的窗口的最上层窗口,默认使用 Activate 方法,通过这个方法在大部分设备都可以做到激活窗口 ...

  4. Quartus prime 的安装步骤:

  5. SpringBoot实现WebSocket发送接收消息 + Vue实现SocketJs接收发送消息

    SpringBoot实现WebSocket发送接收消息 + Vue实现SocketJs接收发送消息 参考: 1.https://www.mchweb.net/index.php/dev/887.htm ...

  6. htts证书申请

    https://freessl.cn/ 教程: https://www.bilibili.com/video/BV1Ug411673P/?spm_id_from=333.337.search-card ...

  7. GPS坐标、火星坐标、百度坐标之间的转换--提供javascript版本转换代码

    1.国内几种常用坐标系说明 WG-S84: GPS仪器记录的经纬度信息,Google Earth采用,Google Map中国范围外使用,高德地图中国范围外使用.GCJ-02: 火星坐标系,中国国家测 ...

  8. StarCoder2-Instruct: 完全透明和可自我对齐的代码生成

    指令微调 是一种技术,它能让大语言模型 (LLMs) 更好地理解和遵循人类的指令.但是,在编程任务中,大多数模型的微调都是基于人类编写的指令 (这需要很高的成本) 或者是由大型专有 LLMs 生成的指 ...

  9. Mark Lee:Splashtop 如何成为最新的 10 亿美元估值技术独角兽

    从左至右:Splashtop联合创始人Rob.Philip.Mark和Thomas Splashtop 刚刚完成了由我们的长期投资者 Sapphire Ventures 领投的 5000 万美元的新融 ...

  10. 2022最新的Centos6.10安装mysql8.0

    一.系统源替换 1.备份系统源 (1)进入源的默认路径 cd /etc/yum.repos.d (2)查看一下 (3)备份 cp CentOS-Base.repo CentOS-Base-Back.r ...