mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: create/update/del follow tag
opt: owner follow page Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user