feat: 搜索建议

This commit is contained in:
guozhigq
2023-06-20 14:23:18 +08:00
parent 335718b3a0
commit 7e7892aab2
13 changed files with 301 additions and 83 deletions

View File

@ -144,4 +144,7 @@ class Api {
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword'; 'https://s.search.bilibili.com/main/hotword';
// 搜索关键词
static const String serachSuggest =
'https://s.search.bilibili.com/main/suggest';
} }

View File

@ -1,5 +1,6 @@
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
class SearchHttp { class SearchHttp {
static Future hotSearchList() async { static Future hotSearchList() async {
@ -17,4 +18,23 @@ class SearchHttp {
}; };
} }
} }
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) {
res.data['result']['term'] = term;
return {
'status': true,
'data': SearchSuggestModel.fromJson(res.data['result']),
};
} else {
return {
'status': false,
'date': [],
'msg': '请求错误 🙅',
};
}
}
} }

View File

@ -0,0 +1,51 @@
class SearchSuggestModel {
SearchSuggestModel({
this.tag,
this.term,
});
List<SearchSuggestItem>? tag;
String? term;
SearchSuggestModel.fromJson(Map<String, dynamic> json) {
tag = json['tag']
.map<SearchSuggestItem>(
(e) => SearchSuggestItem.fromJson(e, json['term']))
.toList();
}
}
class SearchSuggestItem {
SearchSuggestItem({
this.value,
this.term,
this.name,
this.spid,
});
String? value;
String? term;
List? name;
int? spid;
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
value = json['value'];
term = json['term'];
String reg = '<em class=\"suggest_high_light\">$inputTerm</em>';
try {
if (json['name'].indexOf(inputTerm) != -1) {
print(json['name']);
String str = json['name'].replaceAll(reg, '^');
List arr = str.split('^');
arr.insert(arr.length - 1, inputTerm);
name = arr;
} else {
name = ['', '', json['term']];
}
} catch (err) {
name = ['', '', json['term']];
}
spid = json['spid'];
}
}

View File

@ -36,11 +36,14 @@ class HomeAppBar extends StatelessWidget {
), ),
), ),
actions: [ actions: [
IconButton( Hero(
onPressed: () { tag: 'searchTag',
Get.toNamed('/search'); child: IconButton(
}, onPressed: () {
icon: const Icon(CupertinoIcons.search, size: 22), Get.toNamed('/search');
},
icon: const Icon(CupertinoIcons.search, size: 22),
),
), ),
// IconButton( // IconButton(
// onPressed: () {}, // onPressed: () {},

View File

@ -1,24 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class SearchController extends GetxController { class SearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode(); final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs; RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs; Rx<TextEditingController> controller = TextEditingController().obs;
List tabs = [
{'label': '综合', 'id': ''},
{'label': '视频', 'id': ''},
{'label': '番剧', 'id': ''},
{'label': '直播', 'id': ''},
{'label': '专栏', 'id': ''},
{'label': '用户', 'id': ''}
];
List<HotSearchItem> hotSearchList = []; List<HotSearchItem> hotSearchList = [];
Box hotKeyword = GStrorage.hotKeyword; Box hotKeyword = GStrorage.hotKeyword;
RxList<SearchSuggestItem> searchSuggestList = [SearchSuggestItem()].obs;
final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
@override @override
void onInit() { void onInit() {
@ -39,15 +36,26 @@ class SearchController extends GetxController {
void onChange(value) { void onChange(value) {
searchKeyWord.value = value; searchKeyWord.value = value;
if (value == '') {
searchSuggestList.value = [];
return;
}
_debouncer.call(() => querySearchSuggest(value));
} }
void onClear() { void onClear() {
controller.value.clear(); controller.value.clear();
searchKeyWord.value = ''; searchKeyWord.value = '';
searchSuggestList.value = [];
} }
void submit(value) { // 搜索
searchKeyWord.value = value; void submit() {
// ignore: unrelated_type_equality_checks
if (searchKeyWord == '') {
return;
}
Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});
} }
// 获取热搜关键词 // 获取热搜关键词
@ -60,12 +68,19 @@ class SearchController extends GetxController {
// 点击热搜关键词 // 点击热搜关键词
void onClickKeyword(String keyword) { void onClickKeyword(String keyword) {
print(keyword);
searchKeyWord.value = keyword; searchKeyWord.value = keyword;
controller.value.text = keyword; controller.value.text = keyword;
// 移动光标 // 移动光标
controller.value.selection = TextSelection.fromPosition( controller.value.selection = TextSelection.fromPosition(
TextPosition(offset: controller.value.text.length), TextPosition(offset: controller.value.text.length),
); );
submit();
}
Future querySearchSuggest(String value) async {
var result = await SearchHttp.searchSuggest(term: value);
if (result['status']) {
searchSuggestList.value = result['data'].tag;
}
} }
} }

View File

@ -1,8 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/search/index.dart'; import 'controller.dart';
import 'widgets/hotKeyword.dart'; import 'widgets/hotKeyword.dart';
class SearchPage extends StatefulWidget { class SearchPage extends StatefulWidget {
@ -18,6 +18,7 @@ class _SearchPageState extends State<SearchPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
shape: Border( shape: Border(
bottom: BorderSide( bottom: BorderSide(
@ -26,6 +27,14 @@ class _SearchPageState extends State<SearchPage> {
), ),
), ),
titleSpacing: 0, titleSpacing: 0,
actions: [
Hero(
tag: 'searchTag',
child: IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
)
],
title: Obx( title: Obx(
() => TextField( () => TextField(
autofocus: true, autofocus: true,
@ -40,78 +49,78 @@ class _SearchPageState extends State<SearchPage> {
? IconButton( ? IconButton(
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
onPressed: () => _searchController.onClear()) onPressed: () => _searchController.onClear())
: null, : null,
), ),
onSubmitted: (String value) => _searchController.submit(value), onSubmitted: (String value) => _searchController.submit(),
), ),
), ),
), ),
// body: Column( body: Column(
// children: [hotSearch()], children: [
// ), const SizedBox(height: 12),
body: hotSearch(), // 搜索建议
// body: DefaultTabController( _searchSuggest(),
// length: _searchController.tabs.length, // 热搜
// child: Column( hotSearch(),
// children: [ ],
// const SizedBox(height: 4), ),
// Theme( );
// data: ThemeData( }
// splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
// highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 Widget _searchSuggest() {
// ), return Obx(
// child: TabBar( () => _searchController.searchSuggestList.isNotEmpty &&
// tabs: _searchController.tabs _searchController.searchSuggestList.first.term != null
// .map((e) => Tab(text: e['label'])) ? ListView.builder(
// .toList(), physics: const NeverScrollableScrollPhysics(),
// isScrollable: true, shrinkWrap: true,
// indicatorWeight: 0, itemCount: _searchController.searchSuggestList.length,
// indicatorPadding: itemBuilder: (context, index) {
// const EdgeInsets.symmetric(horizontal: 3, vertical: 8), return InkWell(
// indicator: BoxDecoration( customBorder: RoundedRectangleBorder(
// color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4),
// borderRadius: const BorderRadius.all( ),
// Radius.circular(16), onTap: () => _searchController.onClickKeyword(
// ), _searchController.searchSuggestList[index].term!),
// ), child: Padding(
// indicatorSize: TabBarIndicatorSize.tab, padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
// labelColor: Theme.of(context).colorScheme.onSecondaryContainer, // child: Text(
// labelStyle: const TextStyle(fontSize: 13), // _searchController.searchSuggestList[index].term!,
// dividerColor: Colors.transparent, // ),
// unselectedLabelColor: Theme.of(context).colorScheme.outline, child: Text.rich(
// onTap: (index) { TextSpan(
// print(index); children: [
// }, TextSpan(
// ), text: _searchController
// ), .searchSuggestList[index].name![0]),
// Expanded( TextSpan(
// child: TabBarView( text: _searchController
// children: [ .searchSuggestList[index].name![1],
// Container( style: TextStyle(
// width: 200, color: Theme.of(context).colorScheme.primary,
// height: 200, fontWeight: FontWeight.bold),
// color: Colors.amber, ),
// ), TextSpan(
// Text('1'), text: _searchController
// Text('1'), .searchSuggestList[index].name![2]),
// Text('1'), ],
// Text('1'), ),
// Text('1'), ),
// ], ),
// ), );
// ), },
// ], )
// ), : const SizedBox(),
// ),
); );
} }
Widget hotSearch() { Widget hotSearch() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 25, 4, 0), padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -1,5 +1,7 @@
// ignore: file_names // ignore: file_names
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
class HotKeyword extends StatelessWidget { class HotKeyword extends StatelessWidget {
final double? width; final double? width;
@ -44,10 +46,8 @@ class HotKeyword extends StatelessWidget {
if (i.icon != null && i.icon != '') if (i.icon != null && i.icon != '')
SizedBox( SizedBox(
width: 40, width: 40,
child: Image.network( child:
i.icon!, CachedNetworkImage(imageUrl: i.icon!, height: 15.0),
height: 15,
),
), ),
], ],
), ),

View File

@ -0,0 +1,21 @@
import 'package:get/get.dart';
class SearchResultController extends GetxController {
String? keyword;
List tabs = [
{'label': '综合', 'id': ''},
{'label': '视频', 'id': ''},
{'label': '番剧', 'id': ''},
{'label': '直播', 'id': ''},
{'label': '专栏', 'id': ''},
{'label': '用户', 'id': ''}
];
@override
void onInit() {
super.onInit();
if (Get.parameters.keys.isNotEmpty) {
keyword = Get.parameters['keyword'];
}
}
}

View File

@ -0,0 +1,4 @@
library searchresult;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class SearchResultPage extends StatefulWidget {
const SearchResultPage({super.key});
@override
State<SearchResultPage> createState() => _SearchResultPageState();
}
class _SearchResultPageState extends State<SearchResultPage> {
final SearchResultController _searchResultController =
Get.put(SearchResultController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: GestureDetector(
onTap: () => Get.back(),
child: SizedBox(
width: double.infinity,
child: Text(
'${_searchResultController.keyword}',
style: Theme.of(context).textTheme.titleMedium,
),
),
),
),
body: DefaultTabController(
length: _searchResultController.tabs.length,
child: Column(
children: [
Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: TabBar(
tabs: _searchResultController.tabs
.map((e) => Tab(text: e['label']))
.toList(),
isScrollable: true,
indicatorWeight: 0,
indicatorPadding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
indicator: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.onSecondaryContainer,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor: Theme.of(context).colorScheme.outline,
onTap: (index) {
print(index);
},
),
),
Expanded(
child: TabBarView(
children: [
Container(
width: 200,
height: 200,
color: Colors.amber,
),
Text('1'),
Text('1'),
Text('1'),
Text('1'),
Text('1'),
],
),
),
],
),
),
);
}
}

View File

@ -508,7 +508,7 @@ InlineSpan buildContent(BuildContext context, content) {
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => { ..onTap = () => {
Get.toNamed('/search', parameters: { Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title'] 'keyword': content.jumpUrl[matchStr]['title']
}) })
}, },

View File

@ -12,6 +12,8 @@ import 'package:pilipala/pages/webview/index.dart';
import 'package:pilipala/pages/setting/index.dart'; import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import '../pages/searchResult/index.dart';
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
// 首页(推荐) // 首页(推荐)
@ -43,6 +45,8 @@ class Routes {
// 历史记录 // 历史记录
GetPage(name: '/history', page: () => const HistoryPage()), GetPage(name: '/history', page: () => const HistoryPage()),
// 搜索页面 // 搜索页面
GetPage(name: '/search', page: () => const SearchPage()) GetPage(name: '/search', page: () => const SearchPage()),
// 搜索结果
GetPage(name: '/searchResult', page: () => const SearchResultPage())
]; ];
} }

View File

@ -1,4 +1,5 @@
// 工具函数 // 工具函数
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';