diff --git a/lib/http/api.dart b/lib/http/api.dart index 125c5ab0..26ccb2ed 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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'; } diff --git a/lib/http/search.dart b/lib/http/search.dart index d4331ac1..50b8e6b3 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -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': '请求错误 🙅', + }; + } + } } diff --git a/lib/models/search/suggest.dart b/lib/models/search/suggest.dart new file mode 100644 index 00000000..29f5ffe5 --- /dev/null +++ b/lib/models/search/suggest.dart @@ -0,0 +1,51 @@ +class SearchSuggestModel { + SearchSuggestModel({ + this.tag, + this.term, + }); + + List? tag; + String? term; + + SearchSuggestModel.fromJson(Map json) { + tag = json['tag'] + .map( + (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 json, String inputTerm) { + value = json['value']; + term = json['term']; + String reg = '$inputTerm'; + 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']; + } +} diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index c9caf7db..eb93e87e 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -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: () {}, diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 848c4d11..7c6e4976 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -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 controller = TextEditingController().obs; - List tabs = [ - {'label': '综合', 'id': ''}, - {'label': '视频', 'id': ''}, - {'label': '番剧', 'id': ''}, - {'label': '直播', 'id': ''}, - {'label': '专栏', 'id': ''}, - {'label': '用户', 'id': ''} - ]; List hotSearchList = []; Box hotKeyword = GStrorage.hotKeyword; + RxList 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; + } } } diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index d6c550df..ef0d8cdc 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -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 { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, appBar: AppBar( shape: Border( bottom: BorderSide( @@ -26,6 +27,14 @@ class _SearchPageState extends State { ), ), 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 { ? 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: [ diff --git a/lib/pages/search/widgets/hotKeyword.dart b/lib/pages/search/widgets/hotKeyword.dart index 30c2e52a..f5afd3ac 100644 --- a/lib/pages/search/widgets/hotKeyword.dart +++ b/lib/pages/search/widgets/hotKeyword.dart @@ -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), ), ], ), diff --git a/lib/pages/searchResult/controller.dart b/lib/pages/searchResult/controller.dart new file mode 100644 index 00000000..ed7cdad5 --- /dev/null +++ b/lib/pages/searchResult/controller.dart @@ -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']; + } + } +} diff --git a/lib/pages/searchResult/index.dart b/lib/pages/searchResult/index.dart new file mode 100644 index 00000000..c37447d3 --- /dev/null +++ b/lib/pages/searchResult/index.dart @@ -0,0 +1,4 @@ +library searchresult; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/searchResult/view.dart b/lib/pages/searchResult/view.dart new file mode 100644 index 00000000..f62b2352 --- /dev/null +++ b/lib/pages/searchResult/view.dart @@ -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 createState() => _SearchResultPageState(); +} + +class _SearchResultPageState extends State { + 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'), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index f211efbf..d24ad761 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -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'] }) }, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 095e4e8b..6055997f 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -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 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()) ]; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index e03b4b1e..3701e1a5 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,4 +1,5 @@ // 工具函数 +import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:get/get_utils/get_utils.dart';