diff --git a/lib/http/api.dart b/lib/http/api.dart index 2350715fa..fdf94efe9 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -313,4 +313,7 @@ class Api { // 设置Up主分组 // 0 添加至默认分组 否则使用,分割tagid static const String addUsers = '/x/relation/tags/addUsers'; + + // 获取指定分组下的up + static const String followUpGroup = '/x/relation/tag'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index 764d3af02..a48dbffdb 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,5 +1,6 @@ import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/dynamics/result.dart'; +import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/tags.dart'; @@ -184,4 +185,34 @@ class MemberHttp { }; } } + + // 获取某分组下的up + static Future followUpGroup( + int? mid, + int? tagid, + int? pn, + int? ps, + ) async { + var res = await Request().get(Api.followUpGroup, data: { + 'mid': mid, + 'tagid': tagid, + 'pn': pn, + 'ps': ps, + }); + if (res.data['code'] == 0) { + // FollowItemModel + return { + 'status': true, + 'data': res.data['data'] + .map((e) => FollowItemModel.fromJson(e)) + .toList() + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/models/follow/result.dart b/lib/models/follow/result.dart index c6656165f..2f1cedf56 100644 --- a/lib/models/follow/result.dart +++ b/lib/models/follow/result.dart @@ -8,7 +8,7 @@ class FollowDataModel { List? list; FollowDataModel.fromJson(Map json) { - total = json['total']; + total = json['total'] ?? 0; list = json['list'] .map((e) => FollowItemModel.fromJson(e)) .toList(); @@ -19,7 +19,7 @@ class FollowItemModel { FollowItemModel({ this.mid, this.attribute, - this.mtime, + // this.mtime, this.tag, this.special, this.uname, @@ -30,7 +30,7 @@ class FollowItemModel { int? mid; int? attribute; - int? mtime; + // int? mtime; List? tag; int? special; String? uname; @@ -41,7 +41,7 @@ class FollowItemModel { FollowItemModel.fromJson(Map json) { mid = json['mid']; attribute = json['attribute']; - mtime = json['mtime']; + // mtime = json['mtime']; tag = json['tag']; special = json['special']; uname = json['uname']; diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index fe1bfabce..fe4b61004 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -1,20 +1,28 @@ +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/follow.dart'; +import 'package:pilipala/http/member.dart'; import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/utils/storage.dart'; -class FollowController extends GetxController { +/// 查看自己的关注时,可以查看分类 +/// 查看其他人的关注时,只可以看全部 +class FollowController extends GetxController with GetTickerProviderStateMixin { Box userInfoCache = GStrorage.userInfo; int pn = 1; int ps = 20; int total = 0; - RxList followList = [FollowItemModel()].obs; + RxList followList = [].obs; late int mid; late String name; var userInfo; RxString loadingText = '加载中...'.obs; + RxBool isOwner = false.obs; + late List followTags; + late TabController tabController; @override void onInit() { @@ -23,6 +31,7 @@ class FollowController extends GetxController { mid = Get.parameters['mid'] != null ? int.parse(Get.parameters['mid']!) : userInfo.mid; + isOwner.value = mid == userInfo.mid; name = Get.parameters['name'] ?? userInfo.uname; } @@ -56,4 +65,20 @@ class FollowController extends GetxController { } return res; } + + // 当查看当前用户的关注时,请求关注分组 + Future followUpTags() async { + if (userInfo != null && mid == userInfo.mid) { + var res = await MemberHttp.followUpTags(); + if (res['status']) { + followTags = res['data']; + tabController = TabController( + initialIndex: 0, + length: res['data'].length, + vsync: this, + ); + } + return res; + } + } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 0a8cc0efc..a4f1011b5 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,12 +1,8 @@ -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/no_data.dart'; -import 'package:pilipala/models/follow/result.dart'; - import 'controller.dart'; -import 'widgets/follow_item.dart'; +import 'widgets/follow_list.dart'; +import 'widgets/owner_follow_list.dart'; class FollowPage extends StatefulWidget { const FollowPage({super.key}); @@ -19,30 +15,12 @@ class _FollowPageState extends State { late String mid; late FollowController _followController; final ScrollController scrollController = ScrollController(); - Future? _futureBuilderFuture; @override void initState() { super.initState(); mid = Get.parameters['mid']!; _followController = Get.put(FollowController(), tag: mid); - _futureBuilderFuture = _followController.queryFollowings('init'); - scrollController.addListener( - () async { - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('follow', const Duration(seconds: 1), () { - _followController.queryFollowings('onLoad'); - }); - } - }, - ); - } - - @override - void dispose() { - scrollController.removeListener(() {}); - super.dispose(); } @override @@ -54,73 +32,57 @@ class _FollowPageState extends State { titleSpacing: 0, centerTitle: false, title: Text( - '${_followController.name}的关注', + _followController.isOwner.value + ? '我的关注' + : '${_followController.name}的关注', style: Theme.of(context).textTheme.titleMedium, ), ), - body: RefreshIndicator( - onRefresh: () async => - await _followController.queryFollowings('init'), - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - List list = _followController.followList; - return Obx( - () => list.isNotEmpty - ? ListView.builder( - controller: scrollController, - itemCount: list.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == list.length) { - return Container( - height: - MediaQuery.of(context).padding.bottom + - 60, - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .padding - .bottom), - child: Center( - child: Obx( - () => Text( - _followController.loadingText.value, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline, - fontSize: 13), - ), - ), - ), - ); - } else { - return followItem(item: list[index]); - } - }, - ) - : const CustomScrollView( - slivers: [NoData()], + body: Obx( + () => !_followController.isOwner.value + ? FollowList(ctr: _followController) + : FutureBuilder( + future: _followController.followUpTags(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + return Column( + children: [ + TabBar( + controller: _followController.tabController, + isScrollable: true, + tabs: [ + for (var i in data['data']) ...[ + Tab(text: i.name), + ] + ]), + Expanded( + child: TabBarView( + controller: _followController.tabController, + children: [ + for (var i = 0; + i < _followController.tabController.length; + i++) ...[ + OwnerFollowList( + ctr: _followController, + tagItem: _followController.followTags[i], + ) + ] + ], + ), ), - ); - } else { - return CustomScrollView( - slivers: [ - HttpError( - errMsg: data['msg'], - fn: () => _followController.queryFollowings('init'), - ) - ], - ); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - )), + ], + ); + } else { + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), + ), ); } } diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index d9b2617b5..cae72f4c4 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -1,38 +1,45 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/utils.dart'; -Widget followItem({item}) { - String heroTag = Utils.makeHeroTag(item!.mid); - return ListTile( - onTap: () { - feedBack(); - Get.toNamed('/member?mid=${item.mid}', - arguments: {'face': item.face, 'heroTag': heroTag}); - }, - leading: Hero( - tag: heroTag, - child: NetworkImgLayer( - width: 45, - height: 45, - type: 'avatar', - src: item.face, +class FollowItem extends StatelessWidget { + final FollowItemModel item; + const FollowItem({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(item!.mid); + return ListTile( + onTap: () { + feedBack(); + Get.toNamed('/member?mid=${item.mid}', + arguments: {'face': item.face, 'heroTag': heroTag}); + }, + leading: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: item.face, + ), ), - ), - title: Text( - item.uname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - item.sign, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - dense: true, - trailing: const SizedBox(width: 6), - ); + title: Text( + item.uname!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + item.sign!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + dense: true, + trailing: const SizedBox(width: 6), + ); + } } diff --git a/lib/pages/follow/widgets/follow_list.dart b/lib/pages/follow/widgets/follow_list.dart new file mode 100644 index 000000000..73535e2a8 --- /dev/null +++ b/lib/pages/follow/widgets/follow_list.dart @@ -0,0 +1,111 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; +import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/pages/follow/index.dart'; + +import 'follow_item.dart'; + +class FollowList extends StatefulWidget { + final FollowController ctr; + const FollowList({ + super.key, + required this.ctr, + }); + + @override + State createState() => _FollowListState(); +} + +class _FollowListState extends State { + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _futureBuilderFuture = widget.ctr.queryFollowings('init'); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + widget.ctr.queryFollowings('onLoad'); + }); + } + }, + ); + } + + @override + void dispose() { + scrollController.removeListener(() {}); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async => await widget.ctr.queryFollowings('init'), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + List list = widget.ctr.followList; + return Obx( + () => list.isNotEmpty + ? ListView.builder( + controller: scrollController, + itemCount: list.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == list.length) { + return Container( + height: + MediaQuery.of(context).padding.bottom + 60, + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + child: Center( + child: Obx( + () => Text( + widget.ctr.loadingText.value, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: 13), + ), + ), + ), + ); + } else { + return FollowItem(item: list[index]); + } + }, + ) + : const CustomScrollView(slivers: [NoData()]), + ); + } else { + return CustomScrollView( + slivers: [ + HttpError( + errMsg: data['msg'], + fn: () => widget.ctr.queryFollowings('init'), + ) + ], + ); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/follow/widgets/owner_follow_list.dart b/lib/pages/follow/widgets/owner_follow_list.dart new file mode 100644 index 000000000..13a1d0b34 --- /dev/null +++ b/lib/pages/follow/widgets/owner_follow_list.dart @@ -0,0 +1,128 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/models/member/tags.dart'; +import 'package:pilipala/pages/follow/index.dart'; +import 'follow_item.dart'; + +class OwnerFollowList extends StatefulWidget { + final FollowController ctr; + final MemberTagItemModel? tagItem; + const OwnerFollowList({super.key, required this.ctr, this.tagItem}); + + @override + State createState() => _OwnerFollowListState(); +} + +class _OwnerFollowListState extends State + with AutomaticKeepAliveClientMixin { + late int mid; + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + int pn = 1; + int ps = 20; + late MemberTagItemModel tagItem; + RxList followList = [].obs; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + mid = widget.ctr.mid; + tagItem = widget.tagItem!; + _futureBuilderFuture = followUpGroup('init'); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + followUpGroup('onLoad'); + }); + } + }, + ); + } + + // 获取分组下up + Future followUpGroup(type) async { + if (type == 'init') { + pn = 1; + } + var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps); + if (res['status']) { + if (res['data'].isNotEmpty) { + if (type == 'init') { + followList.value = res['data']; + } else { + followList.addAll(res['data']); + } + pn += 1; + } + } + return res; + } + + @override + void dispose() { + scrollController.removeListener(() {}); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return RefreshIndicator( + onRefresh: () async => await followUpGroup('init'), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + return Obx( + () => followList.isNotEmpty + ? ListView.builder( + controller: scrollController, + itemCount: followList.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == followList.length) { + return Container( + height: + MediaQuery.of(context).padding.bottom + 60, + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + ); + } else { + return FollowItem(item: followList[index]); + } + }, + ) + : const CustomScrollView(slivers: [NoData()]), + ); + } else { + return CustomScrollView( + slivers: [ + HttpError( + errMsg: data['msg'], + fn: () => widget.ctr.queryFollowings('init'), + ) + ], + ); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + ); + } +}