feat: create/update/del follow tag

opt: owner follow page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-23 11:12:17 +08:00
parent e212144250
commit 0d27d88719
9 changed files with 405 additions and 87 deletions

View File

@@ -3,13 +3,25 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/follow/result.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/pages/follow/controller.dart';
import 'package:get/get.dart';
enum OrderType { def, attention }
extension OrderTypeExt on OrderType {
String get type => const ['', 'attention'][index];
String get title => const ['最近关注', '最常访问'][index];
}
class FollowChildController
extends CommonListController<List<FollowItemModel>?, FollowItemModel> {
FollowChildController(this.mid, this.tagid);
extends CommonListController<FollowDataModel, FollowItemModel> {
FollowChildController(this.controller, this.mid, this.tagid);
final FollowController controller;
final int? tagid;
final int mid;
late final Rx<OrderType> orderType = OrderType.def.obs;
@override
void onInit() {
super.onInit();
@@ -17,12 +29,35 @@ class FollowChildController
}
@override
Future<LoadingState<List<FollowItemModel>?>> customGetData() {
List<FollowItemModel>? getDataList(FollowDataModel response) {
return response.list;
}
@override
bool customHandleResponse(bool isRefresh, Success<FollowDataModel> response) {
try {
if (controller.isOwner &&
tagid == null &&
isRefresh &&
controller.followState.value is Success) {
controller.tabs[0].count = response.response.total;
controller.tabs.refresh();
}
} catch (_) {}
return false;
}
@override
Future<LoadingState<FollowDataModel>> customGetData() {
if (tagid != null) {
return MemberHttp.followUpGroup(mid, tagid, currentPage, 20);
}
return FollowHttp.followingsNew(
vmid: mid, pn: currentPage, ps: 20, orderType: 'attention');
vmid: mid,
pn: currentPage,
ps: 20,
orderType: orderType.value.type,
);
}
}

View File

@@ -4,14 +4,21 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/follow/result.dart';
import 'package:PiliPlus/pages/follow/child_controller.dart';
import 'package:PiliPlus/pages/follow/controller.dart';
import 'package:PiliPlus/pages/follow/widgets/follow_item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FollowChildPage extends StatefulWidget {
const FollowChildPage({super.key, required this.mid, this.tagid});
const FollowChildPage({
super.key,
required this.controller,
required this.mid,
this.tagid,
});
final FollowController controller;
final int mid;
final int? tagid;
@@ -22,30 +29,50 @@ class FollowChildPage extends StatefulWidget {
class _FollowChildPageState extends State<FollowChildPage>
with AutomaticKeepAliveClientMixin {
late final _followController = Get.put(
FollowChildController(widget.mid, widget.tagid),
FollowChildController(widget.controller, widget.mid, widget.tagid),
tag: Utils.generateRandomString(8));
late final _isOwner = widget.tagid != null;
@override
Widget build(BuildContext context) {
super.build(context);
return refreshIndicator(
onRefresh: () async {
await _followController.onRefresh();
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80),
sliver: Obx(() => _buildBody(_followController.loadingState.value)),
),
],
),
);
if (widget.controller.isOwner && widget.tagid == null) {
return Scaffold(
backgroundColor: Colors.transparent,
body: _child,
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
_followController
..orderType.value =
_followController.orderType.value == OrderType.def
? OrderType.attention
: OrderType.def
..onReload();
},
icon: const Icon(Icons.format_list_bulleted, size: 20),
label: Obx(() => Text(_followController.orderType.value.title)),
),
);
}
return _child;
}
Widget get _child => refreshIndicator(
onRefresh: () async {
await _followController.onRefresh();
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80),
sliver:
Obx(() => _buildBody(_followController.loadingState.value)),
),
],
),
);
Widget _buildBody(LoadingState<List<FollowItemModel>?> loadingState) {
return switch (loadingState) {
Loading() => SliverList.builder(
@@ -63,7 +90,7 @@ class _FollowChildPageState extends State<FollowChildPage>
}
return FollowItem(
item: loadingState.response![index],
isOwner: _isOwner,
isOwner: widget.controller.isOwner,
callback: (attr) {
List<FollowItemModel> list =
(_followController.loadingState.value as Success)
@@ -86,5 +113,5 @@ class _FollowChildPageState extends State<FollowChildPage>
}
@override
bool get wantKeepAlive => widget.tagid != null;
bool get wantKeepAlive => widget.controller.tabController != null;
}

View File

@@ -2,17 +2,17 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/member/tags.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/utils/storage.dart';
class FollowController extends GetxController
with GetSingleTickerProviderStateMixin {
class FollowController extends GetxController with GetTickerProviderStateMixin {
late int mid;
String? name;
late bool isOwner;
late final Rx<LoadingState<List<MemberTagItemModel>?>> followState =
LoadingState<List<MemberTagItemModel>?>.loading().obs;
late final Rx<LoadingState> followState = LoadingState.loading().obs;
late final RxList<MemberTagItemModel> tabs = <MemberTagItemModel>[].obs;
TabController? tabController;
@override
@@ -33,12 +33,20 @@ class FollowController extends GetxController
Future queryFollowUpTags() async {
var res = await MemberHttp.followUpTags();
if (res['status']) {
tabs.clear();
tabs.addAll(res['data']);
tabs.insert(0, MemberTagItemModel(name: '全部关注'));
int initialIndex = 0;
if (tabController != null) {
initialIndex = tabController!.index.clamp(0, tabs.length - 1);
tabController!.dispose();
}
tabController = TabController(
initialIndex: 0,
length: res['data'].length,
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
followState.value = LoadingState.success(res['data']);
followState.value = LoadingState.success(tabs.hashCode);
} else {
followState.value = LoadingState.error(res['msg']);
}
@@ -49,4 +57,37 @@ class FollowController extends GetxController
tabController?.dispose();
super.onClose();
}
Future onCreateTag(String tagName) async {
final res = await MemberHttp.createFollowTag(tagName);
if (res['status']) {
followState.value = LoadingState.loading();
queryFollowUpTags();
SmartDialog.showToast('创建成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
Future onUpdateTag(int index, tagid, String tagName) async {
final res = await MemberHttp.updateFollowTag(tagid, tagName);
if (res['status']) {
tabs[index].name = tagName;
tabs.refresh();
SmartDialog.showToast('修改成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
Future onDelTag(tagid) async {
final res = await MemberHttp.delFollowTag(tagid);
if (res['status']) {
followState.value = LoadingState.loading();
queryFollowUpTags();
SmartDialog.showToast('删除成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
}

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/common/widgets/dialog.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -5,6 +6,7 @@ import 'package:PiliPlus/models/member/tags.dart';
import 'package:PiliPlus/pages/follow/child_view.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'controller.dart';
@@ -16,8 +18,10 @@ class FollowPage extends StatefulWidget {
}
class _FollowPageState extends State<FollowPage> {
final FollowController _followController =
Get.put(FollowController(), tag: Utils.generateRandomString(8));
final FollowController _followController = Get.put(
FollowController(),
tag: Utils.generateRandomString(8),
);
@override
Widget build(BuildContext context) {
@@ -28,6 +32,11 @@ class _FollowPageState extends State<FollowPage> {
),
actions: _followController.isOwner
? [
IconButton(
onPressed: _onCreateTag,
icon: const Icon(Icons.add),
tooltip: '新建分组',
),
IconButton(
onPressed: () => Get.toNamed(
'/followSearch',
@@ -60,50 +69,171 @@ class _FollowPageState extends State<FollowPage> {
),
body: _followController.isOwner
? Obx(() => _buildBody(_followController.followState.value))
: FollowChildPage(mid: _followController.mid),
: FollowChildPage(
controller: _followController, mid: _followController.mid),
);
}
Widget _buildBody(LoadingState<List<MemberTagItemModel>?> loadingState) {
bool _isCustomTag(tagid) {
return tagid != null && tagid != 0 && tagid != -10 && tagid != -2;
}
Widget _buildBody(LoadingState loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success() => loadingState.response?.isNotEmpty == true
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SafeArea(
top: false,
bottom: false,
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
controller: _followController.tabController,
tabs: loadingState.response!
.map((item) => Tab(text: item.name))
.toList(),
),
Success() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SafeArea(
top: false,
bottom: false,
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
controller: _followController.tabController,
tabs: List.generate(_followController.tabs.length, (index) {
return Obx(() {
final item = _followController.tabs[index];
int? count = item.count;
if (_isCustomTag(item.tagid)) {
return GestureDetector(
onLongPress: () {
_onHandleTag(index, item);
},
child: Tab(
child: Row(
children: [
Text(
'${item.name}${count != null ? '($count)' : ''} ',
),
Icon(Icons.menu, size: 18),
],
),
),
);
}
return Tab(
text: '${item.name}${count != null ? '($count)' : ''}');
});
}).toList(),
onTap: (value) {
if (!_followController.tabController!.indexIsChanging) {
final item = _followController.tabs[value];
if (_isCustomTag(item.tagid)) {
_onHandleTag(value, item);
}
}
},
),
),
Expanded(
child: Material(
color: Colors.transparent,
child: tabBarView(
controller: _followController.tabController,
children: _followController.tabs
.map(
(item) => FollowChildPage(
controller: _followController,
mid: _followController.mid,
tagid: item.tagid,
),
)
.toList(),
),
Expanded(
child: Material(
color: Colors.transparent,
child: tabBarView(
controller: _followController.tabController,
children: loadingState.response!
.map(
(item) => FollowChildPage(
mid: _followController.mid,
tagid: item.tagid,
),
)
.toList(),
),
),
),
],
)
: FollowChildPage(mid: _followController.mid),
Error() => FollowChildPage(mid: _followController.mid),
),
),
],
),
Error() => FollowChildPage(
controller: _followController, mid: _followController.mid),
_ => throw UnimplementedError(),
};
}
void _onHandleTag(int index, MemberTagItemModel item) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
Get.back();
String tagName = item.name!;
showConfirmDialog(
context: context,
title: '编辑分组名称',
content: TextFormField(
autofocus: true,
initialValue: tagName,
onChanged: (value) => tagName = value,
inputFormatters: [
LengthLimitingTextInputFormatter(16),
],
decoration:
const InputDecoration(border: OutlineInputBorder()),
),
onConfirm: () {
if (tagName.isNotEmpty) {
_followController.onUpdateTag(
index, item.tagid, tagName);
}
},
);
},
dense: true,
title: const Text(
'修改名称',
style: TextStyle(fontSize: 14),
),
),
ListTile(
onTap: () {
Get.back();
showConfirmDialog(
context: context,
title: '删除分组',
content: '删除后,该分组下的用户依旧保留?',
onConfirm: () {
_followController.onDelTag(item.tagid);
},
);
},
dense: true,
title: const Text(
'删除分组',
style: TextStyle(fontSize: 14),
),
),
],
),
);
},
);
}
void _onCreateTag() {
String tagName = '';
showConfirmDialog(
context: context,
title: '新建分组',
content: TextFormField(
autofocus: true,
initialValue: tagName,
onChanged: (value) => tagName = value,
inputFormatters: [
LengthLimitingTextInputFormatter(16),
],
decoration: const InputDecoration(border: OutlineInputBorder()),
),
onConfirm: () {
_followController.onCreateTag(tagName);
},
);
}
}