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 =
'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/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
class SearchHttp {
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,12 +36,15 @@ class HomeAppBar extends StatelessWidget {
),
),
actions: [
IconButton(
Hero(
tag: 'searchTag',
child: IconButton(
onPressed: () {
Get.toNamed('/search');
},
icon: const Icon(CupertinoIcons.search, size: 22),
),
),
// IconButton(
// onPressed: () {},
// icon: const Icon(CupertinoIcons.bell, size: 22),

View File

@ -1,24 +1,21 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs;
List tabs = [
{'label': '综合', 'id': ''},
{'label': '视频', 'id': ''},
{'label': '番剧', 'id': ''},
{'label': '直播', 'id': ''},
{'label': '专栏', 'id': ''},
{'label': '用户', 'id': ''}
];
List<HotSearchItem> hotSearchList = [];
Box hotKeyword = GStrorage.hotKeyword;
RxList<SearchSuggestItem> searchSuggestList = [SearchSuggestItem()].obs;
final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
@override
void onInit() {
@ -39,15 +36,26 @@ class SearchController extends GetxController {
void onChange(value) {
searchKeyWord.value = value;
if (value == '') {
searchSuggestList.value = [];
return;
}
_debouncer.call(() => querySearchSuggest(value));
}
void onClear() {
controller.value.clear();
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) {
print(keyword);
searchKeyWord.value = keyword;
controller.value.text = keyword;
// 移动光标
controller.value.selection = TextSelection.fromPosition(
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:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/search/index.dart';
import 'controller.dart';
import 'widgets/hotKeyword.dart';
class SearchPage extends StatefulWidget {
@ -18,6 +18,7 @@ class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
shape: Border(
bottom: BorderSide(
@ -26,6 +27,14 @@ class _SearchPageState extends State<SearchPage> {
),
),
titleSpacing: 0,
actions: [
Hero(
tag: 'searchTag',
child: IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
)
],
title: Obx(
() => TextField(
autofocus: true,
@ -40,78 +49,78 @@ class _SearchPageState extends State<SearchPage> {
? IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear())
: null,
),
onSubmitted: (String value) => _searchController.submit(value),
onSubmitted: (String value) => _searchController.submit(),
),
),
),
// body: Column(
// children: [hotSearch()],
// ),
body: hotSearch(),
// body: DefaultTabController(
// length: _searchController.tabs.length,
// child: Column(
// children: [
// const SizedBox(height: 4),
// Theme(
// data: ThemeData(
// splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
// highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
// ),
// child: TabBar(
// tabs: _searchController.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'),
// ],
// ),
// ),
// ],
// ),
body: Column(
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
hotSearch(),
],
),
);
}
Widget _searchSuggest() {
return Obx(
() => _searchController.searchSuggestList.isNotEmpty &&
_searchController.searchSuggestList.first.term != null
? ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _searchController.searchSuggestList.length,
itemBuilder: (context, index) {
return InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
onTap: () => _searchController.onClickKeyword(
_searchController.searchSuggestList[index].term!),
child: Padding(
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
// child: Text(
// _searchController.searchSuggestList[index].term!,
// ),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: _searchController
.searchSuggestList[index].name![0]),
TextSpan(
text: _searchController
.searchSuggestList[index].name![1],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold),
),
TextSpan(
text: _searchController
.searchSuggestList[index].name![2]),
],
),
),
),
);
},
)
: const SizedBox(),
);
}
Widget hotSearch() {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 25, 4, 0),
padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

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

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()
..onTap = () => {
Get.toNamed('/search', parameters: {
Get.toNamed('/searchResult', parameters: {
'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/media/index.dart';
import '../pages/searchResult/index.dart';
class Routes {
static final List<GetPage> getPages = [
// 首页(推荐)
@ -43,6 +45,8 @@ class Routes {
// 历史记录
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:math';
import 'package:get/get_utils/get_utils.dart';