feat: 搜索建议
This commit is contained in:
@ -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';
|
||||
}
|
||||
|
@ -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': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
lib/models/search/suggest.dart
Normal file
51
lib/models/search/suggest.dart
Normal 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'];
|
||||
}
|
||||
}
|
@ -36,11 +36,14 @@ class HomeAppBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.toNamed('/search');
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.search, size: 22),
|
||||
Hero(
|
||||
tag: 'searchTag',
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
Get.toNamed('/search');
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.search, size: 22),
|
||||
),
|
||||
),
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: [
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
21
lib/pages/searchResult/controller.dart
Normal file
21
lib/pages/searchResult/controller.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
}
|
4
lib/pages/searchResult/index.dart
Normal file
4
lib/pages/searchResult/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library searchresult;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
87
lib/pages/searchResult/view.dart
Normal file
87
lib/pages/searchResult/view.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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']
|
||||
})
|
||||
},
|
||||
|
@ -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())
|
||||
];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// 工具函数
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:get/get_utils/get_utils.dart';
|
||||
|
Reference in New Issue
Block a user