home_service_vm.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import 'package:cs_resources/generated/assets.dart';
  2. import 'package:cs_resources/theme/app_colors_theme.dart';
  3. import 'package:domain/entity/garage_sale_rent_entity.dart';
  4. import 'package:domain/entity/newsfeed_detail_entity.dart';
  5. import 'package:domain/entity/paid_service_entity.dart';
  6. import 'package:flutter/cupertino.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:plugin_basic/constants/app_constant.dart';
  9. import 'package:plugin_platform/engine/dialog/dialog_engine.dart';
  10. import 'package:plugin_platform/engine/sp/sp_util.dart';
  11. import 'package:plugin_platform/engine/toast/toast_engine.dart';
  12. import 'package:riverpod_annotation/riverpod_annotation.dart';
  13. import 'package:router/ext/auto_router_extensions.dart';
  14. import 'package:shared/utils/log_utils.dart';
  15. import 'package:widgets/dialog/app_custom_dialog.dart';
  16. import 'package:widgets/load_state_layout.dart';
  17. import 'package:widgets/my_checkbox_group.dart';
  18. import 'package:widgets/picker/option_pick_util.dart';
  19. import 'package:widgets/widget_export.dart';
  20. import '../../../constants_services.dart';
  21. import '../../../respository/services_respository.dart';
  22. import '../../../router/page/services_page_router.dart';
  23. import '../services_main_vm.dart';
  24. import '../services_vm.dart';
  25. import 'home_service_state.dart';
  26. part 'home_service_vm.g.dart';
  27. enum SortByType {
  28. likes_count,
  29. orders_count,
  30. clicks_count,
  31. }
  32. enum SortType {
  33. desc,
  34. asc,
  35. }
  36. const SortType defaultSortType = SortType.desc;
  37. const SortByType defaultSortByType = SortByType.clicks_count;
  38. @riverpod
  39. class HomeServiceVm extends _$HomeServiceVm {
  40. late ServicesRespository servicesRespositoryInstance;
  41. bool _needShowPlaceholder = false; //是否展示LoadingView
  42. int _page = 1; // 当前页
  43. int _limit = 10; // 每页数量
  44. int _count = 0; // 总条数
  45. bool _isSingleSelect = true;
  46. List<Map<String, dynamic>> _currentSelectedCategory = [];
  47. final Map<String, dynamic> _queryParams = {
  48. 'category_id': '',
  49. 'keyword': '',
  50. 'sort': defaultSortType.name, // 排序(desc=降序,asc=升序)
  51. 'sort_by': defaultSortByType.name, // 排序(likes_count=点赞量,orders_count=下单量,clicks_count=点击量)
  52. 'liked': '',
  53. };
  54. // Refresh 控制器
  55. final EasyRefreshController refreshController = EasyRefreshController(
  56. controlFinishRefresh: true, //允许刷新
  57. controlFinishLoad: true, //允许加载
  58. );
  59. HomeServiceState initState() {
  60. return HomeServiceState(
  61. sort: defaultSortType.name,
  62. list: []
  63. );
  64. }
  65. @override
  66. HomeServiceState build(){
  67. // 引入数据仓库
  68. servicesRespositoryInstance = ref.read(servicesRespositoryProvider);
  69. final state = initState();
  70. Log.d("--------------------------build---------------------");
  71. return state;
  72. }
  73. //刷新页面状态
  74. void changeLoadingState(LoadState loadState, String? errorMsg) {
  75. state = state.copyWith(
  76. loadingState: loadState,
  77. errorMessage: errorMsg
  78. );
  79. }
  80. // 初始化页面数据
  81. initPageData() {
  82. Log.d("----home_service_vm-----initPageData ${state.loadingState}");
  83. onRefresh();
  84. }
  85. // 上拉加载 更多
  86. Future loadMore() async {
  87. Log.d("----home_service_vm-----loadMore");
  88. _page++;
  89. getListData();
  90. }
  91. // 下拉刷新
  92. Future onRefresh() async {
  93. // 当前pageView 页面正处于显示状态
  94. Log.d("----forsale_vm-----onRefresh ");
  95. // await Future.delayed(const Duration(seconds: 2));
  96. _page = 1;
  97. getListData();
  98. }
  99. // 手动进行刷新
  100. Future triggerRefresh() async {
  101. Log.d("trggerRefresh");
  102. refreshController.callRefresh();
  103. }
  104. // 手动进行刷新
  105. Future directRefresh() async {
  106. state = state.copyWith(list:[]);
  107. // 注意:由于 nestedscrollview 嵌套easyfresh 组件 refreshController.callRefresh() 自动刷新只能滚动顶部但是不会触发下拉刷新,这里调用是 用到了将其滚动到顶部的作用,进而刷新操作主动掉接口
  108. // https://github.com/xuelongqy/flutter_easy_refresh/issues/692
  109. refreshController.callRefresh();
  110. refreshController.resetFooter();
  111. _page = 1;
  112. _needShowPlaceholder = true;
  113. getListData();
  114. }
  115. // 重试请求
  116. Future retryRequest() async {
  117. _page = 1;
  118. _needShowPlaceholder = true;
  119. getListData();
  120. }
  121. // 获取list 列表数据
  122. Future getListData<T>({bool? isLoadMore}) async {
  123. if (_needShowPlaceholder) {
  124. changeLoadingState(LoadState.State_Loading, null);
  125. }
  126. // List<Map<String, dynamic>> list = [
  127. // {
  128. // 'id':1,
  129. // 'service_type': 0, // 0 房屋保洁 1 空调保洁 2 维修
  130. // 'cover_img': 'https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500',
  131. // 'resources': ['https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500'],
  132. // 'title': 'House Cleaning Services',
  133. // 'price': 30,
  134. // 'unit': '/hr',
  135. // 'liked': true,
  136. // 'likes_count': 12,
  137. // 'company_name': 'HONG YE GROUP PTE LTD',
  138. // },
  139. // {
  140. // 'id':2,
  141. // 'service_type': 1, // 0 房屋保洁 1 空调保洁 2 维修
  142. // 'cover_img': 'https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500',
  143. // 'resources': ['https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500'],
  144. // 'title': 'Air Conditioning Cleaning',
  145. // 'price': 10,
  146. // 'unit': '/unit',
  147. // 'liked': false,
  148. // 'likes_count': 10,
  149. // 'company_name': 'HONG YE GROUP PTE LTD',
  150. // },
  151. // {
  152. // 'id':3,
  153. // 'service_type': 1, // 0 房屋保洁 1 空调保洁 2 维修
  154. // 'cover_img': 'https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500',
  155. // 'resources': ['https://img2.baidu.com/it/u=3489233687,2364672159&fm=253&fmt=auto&app=120&f=JPEG?w=507&h=500'],
  156. // 'title': 'Cleaning Services',
  157. // 'price': 200,
  158. // 'unit': '/hr',
  159. // 'liked': true,
  160. // 'likes_count': 1212,
  161. // 'company_name': 'HONG YE GROUP PTE LTD',
  162. // },
  163. // ];
  164. // handlerResultData(true, list:list);
  165. Log.d("-----55555-------$_queryParams");
  166. try {
  167. //请求网络
  168. Map<String, dynamic> params = {
  169. "sort": _queryParams['sort']?? SortType.desc.name, // 排序(desc=降序,asc=升序)
  170. "sort_by": _queryParams['sort_by']?? SortByType.clicks_count.name, // 排序(likes_count=点赞量,orders_count=下单量,clicks_count=点击量)
  171. "category_id": _queryParams['category_id']??'',
  172. "keyword": _queryParams['keyword']??'',
  173. "liked": _queryParams['liked']??'',
  174. "page": _page,
  175. "limit": _limit,
  176. };
  177. Log.d("请求参数------$params");
  178. final result = await servicesRespositoryInstance.fetchPaidServiceDataList(params);
  179. //校验成功失败
  180. if (result.isSuccess) {
  181. handlerResultList((result.data as PaidServiceEntity)?.list as List<PaidServiceList>?, isLoadMore ?? false);
  182. } else {
  183. String errorMessage = result.errorMsg!;
  184. changeLoadingState(LoadState.State_Error, errorMessage);
  185. ToastEngine.show(result.errorMsg ?? "Network Load Error");
  186. }
  187. } catch (e) {
  188. ToastEngine.show("Error: $e");
  189. }
  190. // 最后赋值
  191. _needShowPlaceholder = false;
  192. }
  193. handlerResultData(bool isList, {List<PaidServiceList>? list, dynamic? data}){
  194. Future.delayed(const Duration(seconds: 1)).then((value) {
  195. if(isList){
  196. // list 数据模式
  197. if(list != null && list.isNotEmpty){
  198. if(_page == 1){
  199. state.list.clear();
  200. state.list!.addAll(list);
  201. refreshController.finishRefresh();
  202. changeLoadingState(LoadState.State_Success, null);
  203. }else {
  204. final allList = state.list;
  205. allList!.addAll(list);
  206. state = state.copyWith(list: allList);
  207. refreshController.finishLoad();
  208. }
  209. }else {
  210. if(_page == 1){
  211. state.list.clear();
  212. changeLoadingState(LoadState.State_Empty, null);
  213. refreshController.finishRefresh();
  214. }else {
  215. refreshController.finishLoad(IndicatorResult.noMore);
  216. }
  217. }
  218. }else {
  219. // 单个数据模式
  220. if(data!=null){
  221. if(_page == 1){
  222. refreshController.finishRefresh();
  223. }else{
  224. refreshController.finishLoad();
  225. }
  226. changeLoadingState(LoadState.State_Success, null);
  227. }else {
  228. if(_page == 1){
  229. refreshController.finishRefresh();
  230. }else{
  231. refreshController.finishLoad();
  232. }
  233. changeLoadingState(LoadState.State_Empty, null);
  234. }
  235. }
  236. });
  237. }
  238. void handlerResultList(List<PaidServiceList>? list, bool isLoadMore) {
  239. if (list != null && list.isNotEmpty) {
  240. //有数据,判断是刷新还是加载更多的数据
  241. if (_page == 1) {
  242. //刷新的方式
  243. state.list!.clear();
  244. state.list!.addAll(list);
  245. refreshController.finishRefresh();
  246. //更新展示的状态
  247. changeLoadingState(LoadState.State_Success, null);
  248. } else {
  249. //加载更多
  250. final allList = state.list;
  251. allList!.addAll(list);
  252. state = state.copyWith(list: allList);
  253. refreshController.finishLoad();
  254. }
  255. } else {
  256. if (_page == 1) {
  257. //展示无数据的布局
  258. state.list!.clear();
  259. changeLoadingState(LoadState.State_Empty, null);
  260. refreshController.finishRefresh();
  261. } else {
  262. //展示加载完成,没有更多数据了
  263. if (_page == 1) {
  264. //展示无数据的布局
  265. state.list!.clear();
  266. changeLoadingState(LoadState.State_Empty, null);
  267. refreshController.finishRefresh();
  268. } else {
  269. //展示加载完成,没有更多数据了
  270. if(state.list!.length == 0){
  271. changeLoadingState(LoadState.State_Empty, null);
  272. refreshController.finishLoad();
  273. }else {
  274. if(_needShowPlaceholder){
  275. changeLoadingState(LoadState.State_Success, null);
  276. }
  277. }
  278. //更新展示的状态
  279. refreshController.finishLoad(IndicatorResult.noMore);
  280. }
  281. }
  282. }
  283. }
  284. // 获取 homeservice 的分类选项
  285. Future<List<Map<String, dynamic>>> getHomeServiceCategoryOptions() async{
  286. // await Future.delayed(const Duration(milliseconds: 300));
  287. // ToastEngine.dismiss();
  288. List<Map<String, dynamic>> serviceCategoryList = [
  289. // {
  290. // 'id': 1,
  291. // 'name': 'House Cleaning Serivces',
  292. // },
  293. // {
  294. // 'id': 2,
  295. // 'name': 'Household Appliance Cleaning',
  296. // },
  297. ];
  298. // 获取分类列表
  299. try {
  300. // 加入有缓存 就取缓存
  301. List<Map<String, dynamic>>? StorageCategoryList = SPUtil.getObjectList(
  302. AppConstant.storagePaidServiceCategoryList)?.cast<Map<String, dynamic>>();
  303. if (StorageCategoryList != null && StorageCategoryList.isNotEmpty) {
  304. Log.d("取StorageCategoryList 缓存: $StorageCategoryList ");
  305. serviceCategoryList = StorageCategoryList;
  306. } else {
  307. // final paidServiceLayoutVm = ref.watch(servicesVmProvider.notifier);
  308. // Map<String, dynamic> params = {
  309. // 'parent_id': paidServiceLayoutVm?.state.parentCategoryId,
  310. // };
  311. final serviceMainVm = ref.read(serviceMainVmProvider.notifier);
  312. Map<String, dynamic> params = {
  313. 'parent_id': serviceMainVm?.state.curId,
  314. };
  315. final result = await servicesRespositoryInstance.fetchServiceCateGoryList(params);
  316. if (result.isSuccess) {
  317. final listJson = result.getListJson();
  318. // 将 listJson 转换为 List<Map<String, dynamic>>
  319. serviceCategoryList = (listJson as List?)
  320. ?.map((item) => item as Map<String, dynamic>)
  321. .toList() ?? [];
  322. // 将 serviceCategoryList 存入缓存
  323. Log.d("设置StorageCategoryList 缓存");
  324. SPUtil.putObjectList(
  325. AppConstant.storagePaidServiceCategoryList, serviceCategoryList);
  326. }
  327. }
  328. } catch(error){
  329. Log.d("获取分类列表失败: $error");
  330. }
  331. return serviceCategoryList;
  332. }
  333. // 点击了filter icon
  334. handlerClickFilterIcon(BuildContext context) async{
  335. List<Map<String, dynamic>> serviceCategoryList = await getHomeServiceCategoryOptions();
  336. // 显示弹框
  337. handlerShowChooseServiceCategoryDialog(context, serviceCategoryList);
  338. }
  339. Future<void> handlerShowChooseServiceCategoryDialog(BuildContext context, List<Map<String, dynamic>> categoryList) async{
  340. await DialogEngine.show(
  341. tag: "chooseServiceCategory",
  342. position: DialogPosition.center,
  343. widget: AppCustomDialog(
  344. message: '',
  345. title: 'Choose a Category',
  346. dialogWidth: MediaQuery.of(context).size.width * 0.8,
  347. // contentBoxMaxHeight: 350,
  348. // contentBoxMinHeight: 300,
  349. isShowConfirmBtn: categoryList!.length > 0 ? true: false,
  350. confirmTxt: "Ok",
  351. messageBuilder: (BuildContext context){
  352. return Container(
  353. child: Column(
  354. mainAxisAlignment: MainAxisAlignment.start,
  355. crossAxisAlignment: CrossAxisAlignment.start,
  356. children: categoryList!.length > 0 ? [
  357. MyCheckboxGroup(
  358. isSingleSelect: _isSingleSelect,
  359. labelStyle: const TextStyle(
  360. fontSize: 16,
  361. fontWeight: FontWeight.w500,
  362. ),
  363. items: categoryList!,
  364. defaultSelectedItems: [],
  365. onChanged: (List<Map<String, dynamic>> selectedItems){
  366. Log.d("----MyCheckboxGroup onChanged $selectedItems");
  367. _currentSelectedCategory = selectedItems;
  368. }
  369. )
  370. ]: [
  371. Container(
  372. child: CircularProgressIndicator(
  373. strokeWidth: 3,
  374. valueColor: AlwaysStoppedAnimation(context.appColors.textDarkGray),
  375. ),
  376. )
  377. ],
  378. ),
  379. );
  380. },
  381. isShowCancelBtn:false,
  382. confirmAction: (){
  383. // 点击了确定
  384. Log.d("----点击了确定按钮 _isSingleSelect:$_isSingleSelect _currentSelectedCategory: $_currentSelectedCategory");
  385. int? categoryId;
  386. if(_isSingleSelect){
  387. if(_currentSelectedCategory.length > 0){
  388. categoryId = _currentSelectedCategory[0]['id'];
  389. _queryParams['categoryId'] = categoryId;
  390. }else {
  391. // 一个都没有选
  392. _queryParams['categoryId'] = '';
  393. }
  394. }
  395. setCurrentQueryParams({
  396. "category_id": _queryParams?['categoryId']??'',
  397. });
  398. directRefresh();
  399. },
  400. )
  401. );
  402. }
  403. // 点击 收藏/取消收藏
  404. Future<bool?> handlerClickCollection(int id, bool? isCollection) async{
  405. try {
  406. //请求网络
  407. Map<String, dynamic> params = {
  408. "id": id,
  409. "type": 'paid', // 类型(paid=付费服务,inquiry=询价服务)
  410. };
  411. Log.d("请求参数------$params");
  412. final result = await servicesRespositoryInstance.fetchServiceLiked(params);
  413. //校验成功失败
  414. if (result.isSuccess) {
  415. // 修改 该id 的 liked 和 likes_count 字段
  416. state.list!.forEach((PaidServiceList elementEntity) {
  417. if(elementEntity.id == id){
  418. elementEntity.liked = !elementEntity.liked!;
  419. elementEntity.likesCount = elementEntity.liked! ? (elementEntity.likesCount! + 1) : (elementEntity.likesCount! - 1);
  420. }
  421. });
  422. return true;
  423. } else {
  424. String errorMessage = result.errorMsg!;
  425. changeLoadingState(LoadState.State_Error, errorMessage);
  426. ToastEngine.show(result.errorMsg ?? "Network Load Error");
  427. }
  428. } catch (e) {
  429. Log.e("Error: $e");
  430. ToastEngine.show("Error: $e");
  431. }
  432. }
  433. // 设置当前的 _queryParams
  434. setCurrentQueryParams(Map<String, dynamic> params){
  435. _queryParams.addAll(params);
  436. }
  437. // 获取当前的 _queryParams
  438. Map<String, dynamic> getCurrentQueryParams(String? key){
  439. if(key!=null && key!.isNotEmpty){
  440. return _queryParams[key];
  441. }
  442. return _queryParams;
  443. }
  444. handlerClickDescSort(BuildContext context, String sort) {
  445. String sortStr = defaultSortType.name;
  446. if(sort == SortType.desc.name){
  447. sortStr = SortType.asc.name;
  448. }else {
  449. sortStr = SortType.desc.name;
  450. }
  451. // 遍历 servicesConstants.sortByOptionsList 找到
  452. _queryParams['sort'] = sortStr;
  453. state = state.copyWith(sort: sortStr);
  454. // 调用list 接口
  455. directRefresh();
  456. }
  457. // 点击 sort icon
  458. handlerClickSortByIcon(BuildContext context) async{
  459. // await SmartDialog.showAttach(
  460. // targetContext: context,
  461. // alignment: Alignment.bottomCenter,
  462. // highlightBuilder: (Offset targetOffset, Size targetSize) {
  463. // Log.d("666 ${targetOffset} ${targetSize}");
  464. // return Positioned(
  465. // right: 0,
  466. // top: 0,
  467. // child: Container(height: targetOffset.dy + 50, width: targetSize.width, color: Colors.white),
  468. // );
  469. // },
  470. // builder: (_) => _listDialog(),
  471. // );
  472. // DialogEngine.showAttach(
  473. // targetContext: context,
  474. // position: DialogPosition.top,
  475. // clickMaskDismiss:false,
  476. // usePenetrate: true,
  477. // widget: _listDialog()
  478. // );
  479. OptionPickerUtil.showCupertinoOptionPicker(
  480. items: state.sortByOptionsList,
  481. initialSelectIndex: 0,
  482. onPickerChanged: (_, index) {
  483. Log.d("----7777--${state.sortByOptionsList?[index]}-----");
  484. String sortByStr = state.sortByOptionsList?[index]??'';
  485. // 遍历 servicesConstants.sortByOptionsList 找到
  486. _queryParams['sort_by'] = servicesConstants.sortByOptionsMap[sortByStr]??'';
  487. state = state.copyWith(sortBySelectedOption: state.sortByOptionsList?[index]);
  488. // 调用list 接口
  489. directRefresh();
  490. },
  491. );
  492. }
  493. Widget _listDialog() {
  494. return Container(
  495. width: 200,
  496. height: 200,
  497. color: Colors.white,
  498. child: Column(
  499. children: [
  500. Text("排序方式"),
  501. Text("价格从低到高"),
  502. Text("价格从高到低"),
  503. Text("距离最近"),
  504. Text("距离最远"),
  505. ],
  506. ),
  507. );
  508. }
  509. // 去详情页面
  510. void handlerGotoDetail({
  511. BuildContext? context,
  512. required int id,
  513. required PaidServiceListCategory paidServiceCategory,
  514. liked,
  515. likesCount,
  516. }){
  517. int cleanServiceTypeId = paidServiceCategory.id!;
  518. Log.d("去详情页面: paidServiceCategory:$paidServiceCategory $id $cleanServiceTypeId");
  519. if(cleanServiceTypeId == servicesConstants.paidServicesType['houseCleaning']!['id'] || cleanServiceTypeId == servicesConstants.paidServicesType['airConditioner']!['id']){
  520. // clean service 跳转到 clean 详情页
  521. appRouter.push(ServiceCleanDetailPageRoute(id: id, cleanServiceTypeId: cleanServiceTypeId,liked: liked,likesCount: likesCount));
  522. }else {
  523. // other service 跳转到 other 详情页
  524. ToastEngine.show("${cleanServiceTypeId} 类型的详情暂未开放");
  525. }
  526. }
  527. }