feat: dyn topic rcmd

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-06-09 19:50:07 +08:00
parent f1e4130201
commit 10efd96788
15 changed files with 260 additions and 142 deletions

View File

@@ -888,4 +888,6 @@ class Api {
static const String vipExpAdd = '/x/vip/experience/add'; static const String vipExpAdd = '/x/vip/experience/add';
static const String coinLog = '/x/member/web/coin/log'; static const String coinLog = '/x/member/web/coin/log';
static const String dynTopicRcmd = '/x/topic/web/dynamic/rcmd';
} }

View File

@@ -14,6 +14,7 @@ import 'package:PiliPlus/models_new/article/article_view/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_reserve/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_reserve/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/topic_card_list.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/topic_card_list.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
@@ -473,4 +474,23 @@ class DynamicsHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
static Future<LoadingState<List<TopicItem>?>> dynTopicRcmd(
{int ps = 25}) async {
final res = await Request().get(
Api.dynTopicRcmd,
queryParameters: {
'source': 'Web',
'page_size': ps,
'web_location': 333.1365,
},
);
if (res.data['code'] == 0) {
return Success((res.data['data']?['topic_items'] as List?)
?.map((e) => TopicItem.fromJson(e))
.toList());
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -1,11 +1,11 @@
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/new_topic.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/new_topic.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/page_info.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/page_info.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
class TopicPubSearchData { class TopicPubSearchData {
NewTopic? newTopic; NewTopic? newTopic;
bool? hasCreateJurisdiction; bool? hasCreateJurisdiction;
List<TopicPubSearchItem>? topicItems; List<TopicItem>? topicItems;
String? requestId; String? requestId;
PageInfo? pageInfo; PageInfo? pageInfo;
@@ -24,7 +24,7 @@ class TopicPubSearchData {
: NewTopic.fromJson(json['new_topic'] as Map<String, dynamic>), : NewTopic.fromJson(json['new_topic'] as Map<String, dynamic>),
hasCreateJurisdiction: json['has_create_jurisdiction'] as bool?, hasCreateJurisdiction: json['has_create_jurisdiction'] as bool?,
topicItems: (json['topic_items'] as List<dynamic>?) topicItems: (json['topic_items'] as List<dynamic>?)
?.map((e) => TopicPubSearchItem.fromJson(e as Map<String, dynamic>)) ?.map((e) => TopicItem.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
requestId: json['request_id'] as String?, requestId: json['request_id'] as String?,
pageInfo: json['page_info'] == null pageInfo: json['page_info'] == null

View File

@@ -1,30 +0,0 @@
class TopicPubSearchItem {
int? id;
String? name;
int? view;
int? discuss;
String? statDesc;
String? description;
bool? showInteractData;
TopicPubSearchItem({
this.id,
this.name,
this.view,
this.discuss,
this.statDesc,
this.description,
this.showInteractData,
});
factory TopicPubSearchItem.fromJson(Map<String, dynamic> json) =>
TopicPubSearchItem(
id: json['id'] as int?,
name: json['name'] as String?,
view: json['view'] as int?,
discuss: json['discuss'] as int?,
statDesc: json['stat_desc'] as String?,
description: json['description'] as String?,
showInteractData: json['show_interact_data'] as bool?,
);
}

View File

@@ -1,10 +1,10 @@
class TopicItem { class TopicItem {
int? id; int id;
String? name; String name;
int? view; int view;
int? discuss; int discuss;
late int fav; int fav;
late int like; int like;
int? dynamics; int? dynamics;
String? jumpUrl; String? jumpUrl;
String? backColor; String? backColor;
@@ -17,10 +17,10 @@ class TopicItem {
bool? isLike; bool? isLike;
TopicItem({ TopicItem({
this.id, required this.id,
this.name, required this.name,
this.view, required this.view,
this.discuss, required this.discuss,
required this.fav, required this.fav,
required this.like, required this.like,
this.dynamics, this.dynamics,
@@ -36,12 +36,12 @@ class TopicItem {
}); });
factory TopicItem.fromJson(Map<String, dynamic> json) => TopicItem( factory TopicItem.fromJson(Map<String, dynamic> json) => TopicItem(
id: json['id'] as int?, id: json['id'],
name: json['name'] as String?, name: json['name'],
view: json['view'] as int? ?? 0, view: json['view'] ?? 0,
discuss: json['discuss'] as int? ?? 0, discuss: json['discuss'] ?? 0,
fav: json['fav'] as int? ?? 0, fav: json['fav'] ?? 0,
like: json['like'] as int? ?? 0, like: json['like'] ?? 0,
dynamics: json['dynamics'] as int?, dynamics: json['dynamics'] as int?,
jumpUrl: json['jump_url'] as String?, jumpUrl: json['jump_url'] as String?,
backColor: json['back_color'] as String?, backColor: json['back_color'] as String?,

View File

@@ -11,7 +11,7 @@ import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/common/reply/reply_option_type.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart'; import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart'; import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/view.dart'; import 'package:PiliPlus/pages/dynamics_select_topic/view.dart';
@@ -621,7 +621,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
double _offset = 0; double _offset = 0;
Future<void> _onSelectTopic() async { Future<void> _onSelectTopic() async {
TopicPubSearchItem? res = await showModalBottomSheet( TopicItem? res = await showModalBottomSheet(
context: context, context: context,
useSafeArea: true, useSafeArea: true,
isScrollControlled: true, isScrollControlled: true,
@@ -643,7 +643,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
), ),
); );
if (res != null) { if (res != null) {
topic.value = Pair(first: res.id!, second: res.name!); topic.value = Pair(first: res.id, second: res.name);
} }
} }
} }

View File

@@ -1,13 +1,13 @@
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class SelectTopicController class SelectTopicController
extends CommonListController<TopicPubSearchData, TopicPubSearchItem> { extends CommonListController<TopicPubSearchData, TopicItem> {
final focusNode = FocusNode(); final focusNode = FocusNode();
final controller = TextEditingController(); final controller = TextEditingController();
@@ -20,7 +20,7 @@ class SelectTopicController
} }
@override @override
List<TopicPubSearchItem>? getDataList(TopicPubSearchData response) { List<TopicItem>? getDataList(TopicPubSearchData response) {
return response.topicItems; return response.topicItems;
} }

View File

@@ -1,12 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart'; import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:stream_transform/stream_transform.dart'; import 'package:stream_transform/stream_transform.dart';
@@ -140,70 +139,41 @@ class _SelectTopicPanelState extends State<SelectTopicPanel> {
} }
Widget _buildBody( Widget _buildBody(
ThemeData theme, LoadingState<List<TopicPubSearchItem>?> loadingState) { ThemeData theme, LoadingState<List<TopicItem>?> loadingState) {
return switch (loadingState) { return switch (loadingState) {
Loading() => loadingWidget, Loading() => loadingWidget,
Success<List<TopicPubSearchItem>?>(:var response) => Success<List<TopicItem>?>(:var response) => response?.isNotEmpty == true
response?.isNotEmpty == true ? NotificationListener<ScrollNotification>(
? NotificationListener<ScrollNotification>( onNotification: (notification) {
onNotification: (notification) { if (notification is UserScrollNotification) {
if (notification is UserScrollNotification) { if (_controller.focusNode.hasFocus) {
if (_controller.focusNode.hasFocus) { _controller.focusNode.unfocus();
_controller.focusNode.unfocus();
}
} else if (notification is ScrollEndNotification) {
widget.callback?.call(notification.metrics.pixels);
} }
return false; } else if (notification is ScrollEndNotification) {
}, widget.callback?.call(notification.metrics.pixels);
child: ListView.builder( }
padding: EdgeInsets.only( return false;
bottom: MediaQuery.paddingOf(context).bottom + },
MediaQuery.viewInsetsOf(context).bottom + child: ListView.builder(
80, padding: EdgeInsets.only(
), bottom: MediaQuery.paddingOf(context).bottom +
controller: widget.scrollController, MediaQuery.viewInsetsOf(context).bottom +
itemBuilder: (context, index) { 80,
if (index == response.length - 1) {
_controller.onLoadMore();
}
final item = response[index];
return ListTile(
dense: true,
onTap: () => Get.back(result: item),
title: Text.rich(
TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 5),
child: Icon(
CustomIcon.topic_tag,
size: 18,
),
),
),
TextSpan(
text: item.name,
style: const TextStyle(fontSize: 14),
),
],
),
),
subtitle: Padding(
padding: const EdgeInsets.only(left: 23),
child: Text(
'${Utils.numFormat(item.view)}浏览 · ${Utils.numFormat(item.discuss)}讨论',
style: TextStyle(color: theme.colorScheme.outline),
),
),
);
},
itemCount: response!.length,
), ),
) controller: widget.scrollController,
: _errWidget(), itemBuilder: (context, index) {
if (index == response.length - 1) {
_controller.onLoadMore();
}
return DynTopicItem(
item: response[index],
onTap: (item) => Get.back(result: item),
);
},
itemCount: response!.length,
),
)
: _errWidget(),
Error(:var errMsg) => _errWidget(errMsg), Error(:var errMsg) => _errWidget(errMsg),
}; };
} }

View File

@@ -0,0 +1,50 @@
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
class DynTopicItem extends StatelessWidget {
const DynTopicItem({
super.key,
required this.item,
required this.onTap,
});
final TopicItem item;
final ValueChanged<TopicItem> onTap;
@override
Widget build(BuildContext context) {
return ListTile(
dense: true,
onTap: () => onTap(item),
title: Text.rich(
TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 5),
child: Icon(
CustomIcon.topic_tag,
size: 18,
),
),
),
TextSpan(
text: item.name,
style: const TextStyle(fontSize: 14),
),
],
),
),
subtitle: Padding(
padding: const EdgeInsets.only(left: 23),
child: Text(
'${Utils.numFormat(item.view)}浏览 · ${Utils.numFormat(item.discuss)}讨论',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@@ -40,7 +40,7 @@ class DynTopicController
topState.value = await DynamicsHttp.topicTop(topicId: topicId); topState.value = await DynamicsHttp.topicTop(topicId: topicId);
if (topState.value.isSuccess) { if (topState.value.isSuccess) {
var topicItem = topState.value.data!.topicItem!; var topicItem = topState.value.data!.topicItem!;
topicName = topicItem.name!; topicName = topicItem.name;
isFav.value = topicItem.isFav; isFav.value = topicItem.isFav;
isLike.value = topicItem.isLike; isLike.value = topicItem.isLike;
} }

View File

@@ -153,7 +153,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
pinned: true, pinned: true,
callback: (value) => _controller.appbarOffset = callback: (value) => _controller.appbarOffset =
value - kToolbarHeight - paddingTop - 7, value - kToolbarHeight - paddingTop - 7,
title: IgnorePointer(child: Text(response!.topicItem!.name!)), title: IgnorePointer(child: Text(response!.topicItem!.name)),
flexibleSpace: Container( flexibleSpace: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
@@ -206,7 +206,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
), ),
), ),
Text( Text(
response.topicItem!.name!, response.topicItem!.name,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -0,0 +1,17 @@
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
class DynTopicRcmdController
extends CommonListController<List<TopicItem>?, TopicItem> {
@override
void onInit() {
super.onInit();
queryData();
}
@override
Future<LoadingState<List<TopicItem>?>> customGetData() =>
DynamicsHttp.dynTopicRcmd();
}

View File

@@ -0,0 +1,70 @@
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart';
import 'package:PiliPlus/pages/dynamics_topic_rcmd/controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class DynTopicRcmdPage extends StatefulWidget {
const DynTopicRcmdPage({super.key});
@override
State<DynTopicRcmdPage> createState() => _DynTopicRcmdPageState();
}
class _DynTopicRcmdPageState extends State<DynTopicRcmdPage> {
final DynTopicRcmdController _controller = Get.put(DynTopicRcmdController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('话题')),
body: SafeArea(
top: false,
bottom: false,
child: refreshIndicator(
onRefresh: _controller.onRefresh,
child: CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
),
],
),
),
),
);
}
Widget _buildBody(LoadingState<List<TopicItem>?> loadingState) {
return switch (loadingState) {
Loading() => const SliverToBoxAdapter(child: LinearProgressIndicator()),
Success(:var response) => response?.isNotEmpty == true
? SliverList.builder(
itemCount: response!.length,
itemBuilder: (context, index) {
return DynTopicItem(
item: response[index],
onTap: (item) => Get.toNamed(
'/dynTopic',
parameters: {
'id': item.id.toString(),
'name': item.name,
},
),
);
},
)
: HttpError(onReload: _controller.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _controller.onReload,
),
};
}
}

View File

@@ -174,31 +174,48 @@ class _SearchPageState extends State<SearchPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
text, text,
Padding( const SizedBox(width: 14),
padding: const EdgeInsets.only(left: 14), SizedBox(
child: SizedBox( height: 34,
height: 34, child: TextButton.icon(
child: TextButton.icon( onPressed: () => Get.toNamed(
onPressed: () => Get.toNamed( '/searchTrending',
'/searchTrending', parameters: {'tag': _tag},
parameters: {'tag': _tag}, ),
), label: Text(
label: Text( '完整榜单',
'完整榜单', style: TextStyle(
style: TextStyle( fontSize: 13,
fontSize: 13,
color: theme.colorScheme.outline,
),
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline, color: theme.colorScheme.outline,
), ),
iconAlignment: IconAlignment.end,
), ),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
iconAlignment: IconAlignment.end,
), ),
) ),
SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed('/dynTopicRcmd'),
label: Text(
'话题',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
iconAlignment: IconAlignment.end,
),
),
], ],
) )
: text, : text,

View File

@@ -6,6 +6,7 @@ import 'package:PiliPlus/pages/danmaku_block/view.dart';
import 'package:PiliPlus/pages/dynamics/view.dart'; import 'package:PiliPlus/pages/dynamics/view.dart';
import 'package:PiliPlus/pages/dynamics_detail/view.dart'; import 'package:PiliPlus/pages/dynamics_detail/view.dart';
import 'package:PiliPlus/pages/dynamics_topic/view.dart'; import 'package:PiliPlus/pages/dynamics_topic/view.dart';
import 'package:PiliPlus/pages/dynamics_topic_rcmd/view.dart';
import 'package:PiliPlus/pages/fan/view.dart'; import 'package:PiliPlus/pages/fan/view.dart';
import 'package:PiliPlus/pages/fav/view.dart'; import 'package:PiliPlus/pages/fav/view.dart';
import 'package:PiliPlus/pages/fav_create/view.dart'; import 'package:PiliPlus/pages/fav_create/view.dart';
@@ -177,6 +178,7 @@ class Routes {
CustomGetPage(name: '/barSetting', page: () => const BarSetPage()), CustomGetPage(name: '/barSetting', page: () => const BarSetPage()),
CustomGetPage(name: '/upowerRank', page: () => const UpowerRankPage()), CustomGetPage(name: '/upowerRank', page: () => const UpowerRankPage()),
CustomGetPage(name: '/spaceSetting', page: () => const SpaceSettingPage()), CustomGetPage(name: '/spaceSetting', page: () => const SpaceSettingPage()),
CustomGetPage(name: '/dynTopicRcmd', page: () => const DynTopicRcmdPage()),
]; ];
} }