search_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_easyrefresh/easy_refresh.dart';
  3. import 'package:provider/provider.dart';
  4. import 'package:sport/bean/post.dart';
  5. import 'package:sport/bean/post_user.dart';
  6. import 'package:sport/bean/user_friend.dart';
  7. import 'package:sport/bean/user_info.dart';
  8. import 'package:sport/pages/social/post_widget.dart';
  9. import 'package:sport/pages/social/user_detail_page.dart';
  10. import 'package:sport/provider/lib/provider_widget.dart' as p;
  11. import 'package:sport/provider/lib/provider_widget_selector.dart';
  12. import 'package:sport/provider/lib/simple_model.dart';
  13. import 'package:sport/provider/search_model.dart';
  14. import 'package:sport/provider/social_detail_model.dart';
  15. import 'package:sport/router/navigator_util.dart';
  16. import 'package:sport/services/api/inject_api.dart';
  17. import 'package:sport/services/api/resp.dart';
  18. import 'package:sport/services/userid.dart';
  19. import 'package:sport/utils/click.dart';
  20. import 'package:sport/utils/toast.dart';
  21. import 'package:sport/widgets/appbar.dart';
  22. import 'package:sport/widgets/dialog/request_dialog.dart';
  23. import 'package:sport/widgets/error.dart';
  24. import 'package:sport/widgets/image.dart';
  25. import 'package:sport/widgets/loading.dart';
  26. import 'package:sport/widgets/misc.dart';
  27. import 'package:sport/widgets/space.dart';
  28. class SearchPage extends StatefulWidget {
  29. @override
  30. State<StatefulWidget> createState() => _PageState();
  31. }
  32. class _PageState extends State<SearchPage> with UserId, InjectApi {
  33. late TextEditingController _controller;
  34. late FocusNode _focusNode;
  35. late SearchModel _model;
  36. late SocialDetailModel _searchModel;
  37. final double tabHeader = 50;
  38. late SimpleModel simpleModel;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _focusNode = FocusNode();
  43. _controller = new TextEditingController(text: '');
  44. _model = SearchModel()
  45. ..getHistory()
  46. ..getHot();
  47. _searchModel = SocialDetailModel(
  48. 100,
  49. );
  50. simpleModel = new SimpleModel((page) async {
  51. var list = (await api.userSearch(kw: _controller.text, page: page)).pageResult.results;
  52. return list ?? [];
  53. });
  54. }
  55. @override
  56. void dispose() {
  57. super.dispose();
  58. _focusNode.dispose();
  59. _controller.dispose();
  60. _searchModel.dispose();
  61. }
  62. _submitValue(String value) {
  63. // _controller.text = value;
  64. _model.queryValue(value);
  65. _searchModel.setKeyword(value);
  66. simpleModel.loadData();
  67. // _focusNode?.unfocus();
  68. }
  69. @override
  70. Widget build(BuildContext context) {
  71. return ProviderWidget<SearchModel, SearchBody>(
  72. model: _model,
  73. selector: (_, model) {
  74. return model.currentBody;
  75. },
  76. shouldRebuild: (_, __) => true,
  77. builder: (BuildContext context, _body, Widget? child) {
  78. Widget body;
  79. switch (_body) {
  80. case SearchBody.defaultBody:
  81. body = _buildDefaultWidget();
  82. break;
  83. case SearchBody.suggestions:
  84. // body = buildSuggestions(context);
  85. // break;
  86. case SearchBody.results:
  87. body = buildResults(context);
  88. break;
  89. }
  90. return Scaffold(
  91. backgroundColor: Colors.white,
  92. appBar: AppBar(
  93. titleSpacing: 0,
  94. centerTitle: false,
  95. automaticallyImplyLeading: false,
  96. title: Container(
  97. margin: EdgeInsets.fromLTRB(12.0, 0, 6.0, 0),
  98. height: 35,
  99. decoration: BoxDecoration(
  100. color: Color(0xffF1F1F1),
  101. shape: BoxShape.rectangle,
  102. borderRadius: BorderRadius.all(Radius.circular(50)),
  103. ),
  104. child: Row(
  105. children: <Widget>[
  106. Space(
  107. width: 12,
  108. ),
  109. Image.asset("lib/assets/img/searchbar_icon_search.png"),
  110. Space(
  111. width: 6,
  112. ),
  113. Expanded(
  114. child: Selector<SearchModel, String>(
  115. selector: (_, model) => model.searchValue,
  116. builder: (context, _value, child) => Container(
  117. constraints: BoxConstraints(maxHeight: 30),
  118. child: TextField(
  119. controller: _controller,
  120. maxLines: 1,
  121. focusNode: _focusNode,
  122. decoration: InputDecoration(
  123. hintText: '请输入搜索内容',
  124. contentPadding: const EdgeInsets.symmetric(vertical: 4.0),
  125. border: OutlineInputBorder(borderSide: BorderSide.none),
  126. hintStyle: TextStyle(color: Color(0xff999999))),
  127. onChanged: debounceValueChanged((value) {
  128. if (value != "") {
  129. _submitValue(value);
  130. }
  131. _model.queryValue(value);
  132. Provider.of<SearchModel>(context, listen: false).updateSearchValue(value);
  133. }),
  134. onSubmitted: (value) {
  135. _submitValue(value);
  136. // _searchModel.setKeyword(value);
  137. // Provider.of<SearchModel>(context, listen: false).queryValue(value);
  138. },
  139. ),
  140. ),
  141. )),
  142. Visibility(
  143. visible: context.select<SearchModel, String>((value) => value.searchValue).isNotEmpty,
  144. child: GestureDetector(
  145. onTap: () {
  146. Provider.of<SearchModel>(context, listen: false).updateSearchValue("");
  147. _controller.clear();
  148. },
  149. child: Padding(
  150. padding: const EdgeInsets.all(8.0),
  151. child: Image.asset("lib/assets/img/searchbar_btn_no.png"),
  152. ),
  153. ))
  154. ],
  155. ),
  156. ),
  157. actions: <Widget>[
  158. buildActionButton("取消", () {
  159. Navigator.of(context).pop();
  160. }, textColor: Color(0xff333333)),
  161. ],
  162. ),
  163. body: AnimatedSwitcher(
  164. duration: const Duration(milliseconds: 300),
  165. child: body,
  166. ),
  167. );
  168. },
  169. );
  170. }
  171. Widget _buildDefaultWidget() {
  172. return CustomScrollView(
  173. slivers: <Widget>[
  174. SliverPadding(
  175. padding: EdgeInsets.all(12.0),
  176. sliver: SliverToBoxAdapter(
  177. child: Selector<SearchModel, List>(
  178. selector: (_, model) => model.hot,
  179. builder: (context, _value, child) {
  180. if (_value.isNotEmpty) {
  181. return Column(
  182. crossAxisAlignment: CrossAxisAlignment.start,
  183. children: <Widget>[
  184. Text("热门帖子", style: Theme.of(context).textTheme.headline3!.copyWith(fontSize: 18)),
  185. Padding(
  186. padding: const EdgeInsets.symmetric(vertical: 6.0),
  187. child: Wrap(
  188. spacing: 12,
  189. children: _value.take(10).map((e) {
  190. return GestureDetector(
  191. onTap: () {
  192. _controller.text = e;
  193. _submitValue(e);
  194. // _model.queryValue(e);
  195. },
  196. child: Chip(
  197. labelPadding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0),
  198. label: Text("$e"),
  199. labelStyle: TextStyle(color: Color(0xff666666)),
  200. backgroundColor: Colors.white,
  201. shape: StadiumBorder(
  202. side: BorderSide(color: Color(0xffDCDCDC), width: 0.5),
  203. ),
  204. ));
  205. }).toList()),
  206. ),
  207. ],
  208. );
  209. }
  210. return Container();
  211. },
  212. ),
  213. ),
  214. ),
  215. SliverPadding(
  216. padding: EdgeInsets.all(12.0),
  217. sliver: SliverToBoxAdapter(
  218. child: Selector<SearchModel, List>(
  219. selector: (_, model) => model.history,
  220. builder: (context, _value, child) {
  221. if (_value != null && _value.isNotEmpty) {
  222. return Column(
  223. crossAxisAlignment: CrossAxisAlignment.start,
  224. children: <Widget>[
  225. Row(
  226. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  227. children: <Widget>[
  228. Text("历史搜索", style: Theme.of(context).textTheme.headline3!.copyWith(fontSize: 18)),
  229. InkWell(
  230. onTap: () {
  231. _model.clearHistory();
  232. },
  233. child: Padding(
  234. padding: const EdgeInsets.all(8.0),
  235. child: Image.asset("lib/assets/img/list_icon_del.png"),
  236. ),
  237. )
  238. ],
  239. ),
  240. Padding(
  241. padding: const EdgeInsets.symmetric(vertical: 6.0),
  242. child: Wrap(
  243. alignment: WrapAlignment.spaceBetween,
  244. spacing: 12,
  245. children: _value.map((e) {
  246. return GestureDetector(
  247. onTap: () {
  248. _controller.text = e;
  249. _submitValue(e);
  250. // _model.queryValue(e);
  251. },
  252. child: Chip(
  253. labelPadding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0),
  254. label: Text("$e"),
  255. labelStyle: TextStyle(color: Color(0xff666666)),
  256. backgroundColor: Colors.white,
  257. shape: StadiumBorder(
  258. side: BorderSide(color: Color(0xffDCDCDC), width: 0.5),
  259. ),
  260. ));
  261. }).toList(),
  262. ),
  263. ),
  264. ],
  265. );
  266. }
  267. return Container();
  268. },
  269. ),
  270. ),
  271. )
  272. ],
  273. );
  274. }
  275. Widget buildSuggestions(BuildContext context) {
  276. return FutureBuilder(
  277. future: _model.api.getPostList(kw: _model.searchValue),
  278. builder: (context, AsyncSnapshot<RespPage<Post>> snapshot) => snapshot.connectionState == ConnectionState.done
  279. ? snapshot.data?.code == 0
  280. ? snapshot.data?.pageResult.results?.isEmpty == true
  281. ? Center(
  282. child: RequestErrorWidget(
  283. null,
  284. msg: "暂时找不到您想搜索的东西喔",
  285. ))
  286. : ListView.separated(
  287. itemBuilder: (context, index) {
  288. var item = snapshot.data!.pageResult.results![index];
  289. return ListTile(
  290. title: Text(
  291. item.content ?? "",
  292. maxLines: 1,
  293. overflow: TextOverflow.ellipsis,
  294. ),
  295. onTap: () {
  296. // query = '老孟 $index';
  297. String value = item.content ?? "";
  298. _submitValue(value);
  299. // NavigatorUtil.goSocialPostDetail(context, item);
  300. },
  301. trailing: arrowRight(),
  302. );
  303. },
  304. separatorBuilder: (context, index) {
  305. return Divider(
  306. height: 1,
  307. indent: 12,
  308. endIndent: 12,
  309. );
  310. },
  311. itemCount: snapshot.data!.pageResult.results!.length,
  312. )
  313. : Container()
  314. : Container(),
  315. );
  316. }
  317. Widget buildResults(BuildContext context) {
  318. return DefaultTabController(
  319. length: 2,
  320. child: Column(
  321. children: <Widget>[
  322. Container(
  323. color: Colors.white,
  324. height: tabHeader,
  325. padding: EdgeInsets.symmetric(vertical: 8.0),
  326. child: TabBar(
  327. isScrollable: true,
  328. indicatorPadding: EdgeInsets.symmetric(horizontal: 6),
  329. indicatorWeight: 3,
  330. tabs: <Widget>[
  331. Tab(text: "帖子"),
  332. Tab(text: "用户"),
  333. ],
  334. ),
  335. alignment: Alignment.centerLeft,
  336. ),
  337. Expanded(
  338. child: TabBarView(
  339. children: <Widget>[
  340. p.ProviderWidget<SocialDetailModel>(
  341. model: _searchModel,
  342. autoDispose: false, // 自动 dispose
  343. builder: (_, model, __) {
  344. return EasyRefresh.custom(
  345. controller: model.refreshController,
  346. enableControlFinishRefresh: true,
  347. enableControlFinishLoad: true,
  348. onLoad: model.isIdle ? () => model.loadMore() : null,
  349. header: buildClassicalHeader(),
  350. footer: buildClassicalFooter(),
  351. slivers: <Widget>[
  352. if (model.isBusy)
  353. SliverToBoxAdapter(
  354. child: RequestLoadingWidget(),
  355. ),
  356. if (model.isEmpty)
  357. SliverToBoxAdapter(
  358. child: RequestErrorWidget(
  359. null,
  360. msg: "暂时找不到您想搜索的东西喔",
  361. assets: RequestErrorWidget.ASSETS_NO_MOTION,
  362. ),
  363. ),
  364. SliverList(
  365. delegate: SliverChildBuilderDelegate(
  366. (context, index) {
  367. Post post = model.list[index];
  368. return PostWidget(
  369. post,
  370. _searchModel,
  371. selfId == post.userId,
  372. keyword: _controller.text,
  373. highlight: true,
  374. );
  375. },
  376. childCount: model.list.length,
  377. ),
  378. ),
  379. ],
  380. );
  381. },
  382. ),
  383. p.ProviderWidget<SimpleModel>(
  384. model: simpleModel,
  385. onModelReady: (model) => model.initData(),
  386. autoDispose: false,
  387. builder: (_, model, __) {
  388. return EasyRefresh.custom(
  389. // firstRefresh: true,
  390. controller: model.refreshController,
  391. enableControlFinishRefresh: true,
  392. enableControlFinishLoad: true,
  393. onLoad: model.isIdle ? () => model.initData() : null,
  394. header: buildClassicalHeader(),
  395. footer: buildClassicalFooter(),
  396. slivers: <Widget>[
  397. if (model.isBusy)
  398. SliverToBoxAdapter(
  399. child: RequestLoadingWidget(),
  400. ),
  401. if (model.isEmpty || model.isError)
  402. SliverToBoxAdapter(
  403. child: RequestErrorWidget(
  404. null,
  405. msg: _model.searchValue == "" ? "请输入搜索的关键字1" : "暂无相关用户~",
  406. ),
  407. ),
  408. if (model.isIdle)
  409. SliverList(
  410. delegate: SliverChildBuilderDelegate(
  411. (context, index) {
  412. return _buildItem(model.list[index]);
  413. },
  414. childCount: model.list.length,
  415. ),
  416. ),
  417. ],
  418. );
  419. },
  420. ),
  421. ],
  422. ),
  423. )
  424. ],
  425. ),
  426. );
  427. }
  428. Widget _buildItem(UserInfo user) {
  429. Widget child = Row(
  430. children: <Widget>[
  431. CircleAvatar(
  432. backgroundColor: Colors.black26,
  433. backgroundImage: userAvatarProvider(user.avatar),
  434. radius: 22,
  435. ),
  436. SizedBox(
  437. width: 8,
  438. ),
  439. Expanded(
  440. child: Column(
  441. crossAxisAlignment: CrossAxisAlignment.start,
  442. children: <Widget>[
  443. Text(
  444. "${user.name}",
  445. style: Theme.of(context).textTheme.headline3,
  446. ),
  447. SizedBox(
  448. height: 4,
  449. ),
  450. Text(
  451. "ID: ${user.id}",
  452. style: Theme.of(context).textTheme.bodyText2!,
  453. ),
  454. ],
  455. ),
  456. ),
  457. user.isFriend()
  458. ? Container(
  459. width: 64,
  460. height: 30,
  461. margin: EdgeInsets.only(left: 8.0),
  462. alignment: Alignment.center,
  463. child: Text(
  464. "已关注",
  465. strutStyle: fixedLine,
  466. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Theme.of(context).accentColor),
  467. ),
  468. )
  469. : GestureDetector(
  470. child: Container(
  471. width: 64,
  472. height: 30,
  473. margin: EdgeInsets.only(left: 8.0),
  474. alignment: Alignment.center,
  475. child: Text(
  476. "关注",
  477. strutStyle: fixedLine,
  478. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Theme.of(context).accentColor),
  479. ),
  480. decoration: BoxDecoration(
  481. borderRadius: BorderRadius.circular(20),
  482. border: Border.all(
  483. color: Theme.of(context).accentColor,
  484. width: .5,
  485. ),
  486. ),
  487. ),
  488. onTap: () async {
  489. if (user.isFriend()) return;
  490. await request(context, () async {
  491. var resp = await simpleModel.api.userFollow(uid: user.id).catchError((onError) {});
  492. if (resp.code == 0) {
  493. ToastUtil.show("关注成功");
  494. setState(() {
  495. user.followStatus = "followed";
  496. });
  497. }
  498. });
  499. },
  500. )
  501. ],
  502. );
  503. return Column(
  504. children: <Widget>[
  505. Padding(
  506. padding: const EdgeInsets.all(12.0),
  507. child: InkWell(
  508. onTap: () async {
  509. List<UserFriend> friends =
  510. simpleModel.list.map((e) => UserFriend(uid: user.id, socialInfo: user, isFriends: user.isFriend() ? "1" : "0")).toList();
  511. await NavigatorUtil.goTransparentPage(
  512. context, (context,_,__) => UserDetailPage(PostUser(id: "${user.id}", name: user.name, avatar: user.avatar), userFriends: friends));
  513. user.followStatus = friends.firstWhere((element) => element.uid == user.id).isFriends == "1" ? "followed" : "none";
  514. setState(() {});
  515. },
  516. child: child),
  517. ),
  518. Divider(
  519. height: 1,
  520. )
  521. ],
  522. );
  523. }
  524. }
  525. class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  526. final TabBar child;
  527. StickyTabBarDelegate({required this.child});
  528. @override
  529. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
  530. return this.child;
  531. }
  532. @override
  533. double get maxExtent => this.child.preferredSize.height;
  534. @override
  535. double get minExtent => this.child.preferredSize.height;
  536. @override
  537. bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
  538. return true;
  539. }
  540. }