search_page.dart 22 KB

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