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 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_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/topic_item.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
@@ -473,4 +474,23 @@ class DynamicsHttp {
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/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 {
NewTopic? newTopic;
bool? hasCreateJurisdiction;
List<TopicPubSearchItem>? topicItems;
List<TopicItem>? topicItems;
String? requestId;
PageInfo? pageInfo;
@@ -24,7 +24,7 @@ class TopicPubSearchData {
: NewTopic.fromJson(json['new_topic'] as Map<String, dynamic>),
hasCreateJurisdiction: json['has_create_jurisdiction'] as bool?,
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(),
requestId: json['request_id'] as String?,
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 {
int? id;
String? name;
int? view;
int? discuss;
late int fav;
late int like;
int id;
String name;
int view;
int discuss;
int fav;
int like;
int? dynamics;
String? jumpUrl;
String? backColor;
@@ -17,10 +17,10 @@ class TopicItem {
bool? isLike;
TopicItem({
this.id,
this.name,
this.view,
this.discuss,
required this.id,
required this.name,
required this.view,
required this.discuss,
required this.fav,
required this.like,
this.dynamics,
@@ -36,12 +36,12 @@ class TopicItem {
});
factory TopicItem.fromJson(Map<String, dynamic> json) => TopicItem(
id: json['id'] as int?,
name: json['name'] as String?,
view: json['view'] as int? ?? 0,
discuss: json['discuss'] as int? ?? 0,
fav: json['fav'] as int? ?? 0,
like: json['like'] as int? ?? 0,
id: json['id'],
name: json['name'],
view: json['view'] ?? 0,
discuss: json['discuss'] ?? 0,
fav: json['fav'] ?? 0,
like: json['like'] ?? 0,
dynamics: json['dynamics'] as int?,
jumpUrl: json['jump_url'] 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/models/common/publish_panel_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/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/view.dart';
@@ -621,7 +621,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
double _offset = 0;
Future<void> _onSelectTopic() async {
TopicPubSearchItem? res = await showModalBottomSheet(
TopicItem? res = await showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
@@ -643,7 +643,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
),
);
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/search.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:flutter/widgets.dart';
import 'package:get/get.dart';
class SelectTopicController
extends CommonListController<TopicPubSearchData, TopicPubSearchItem> {
extends CommonListController<TopicPubSearchData, TopicItem> {
final focusNode = FocusNode();
final controller = TextEditingController();
@@ -20,7 +20,7 @@ class SelectTopicController
}
@override
List<TopicPubSearchItem>? getDataList(TopicPubSearchData response) {
List<TopicItem>? getDataList(TopicPubSearchData response) {
return response.topicItems;
}

View File

@@ -1,12 +1,11 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.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/widgets/item.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:stream_transform/stream_transform.dart';
@@ -140,70 +139,41 @@ class _SelectTopicPanelState extends State<SelectTopicPanel> {
}
Widget _buildBody(
ThemeData theme, LoadingState<List<TopicPubSearchItem>?> loadingState) {
ThemeData theme, LoadingState<List<TopicItem>?> loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success<List<TopicPubSearchItem>?>(:var response) =>
response?.isNotEmpty == true
? NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is UserScrollNotification) {
if (_controller.focusNode.hasFocus) {
_controller.focusNode.unfocus();
}
} else if (notification is ScrollEndNotification) {
widget.callback?.call(notification.metrics.pixels);
Success<List<TopicItem>?>(:var response) => response?.isNotEmpty == true
? NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is UserScrollNotification) {
if (_controller.focusNode.hasFocus) {
_controller.focusNode.unfocus();
}
return false;
},
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom +
MediaQuery.viewInsetsOf(context).bottom +
80,
),
controller: widget.scrollController,
itemBuilder: (context, index) {
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,
} else if (notification is ScrollEndNotification) {
widget.callback?.call(notification.metrics.pixels);
}
return false;
},
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom +
MediaQuery.viewInsetsOf(context).bottom +
80,
),
)
: _errWidget(),
controller: widget.scrollController,
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),
};
}

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);
if (topState.value.isSuccess) {
var topicItem = topState.value.data!.topicItem!;
topicName = topicItem.name!;
topicName = topicItem.name;
isFav.value = topicItem.isFav;
isLike.value = topicItem.isLike;
}

View File

@@ -153,7 +153,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
pinned: true,
callback: (value) => _controller.appbarOffset =
value - kToolbarHeight - paddingTop - 7,
title: IgnorePointer(child: Text(response!.topicItem!.name!)),
title: IgnorePointer(child: Text(response!.topicItem!.name)),
flexibleSpace: Container(
decoration: BoxDecoration(
image: DecorationImage(
@@ -206,7 +206,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
),
),
Text(
response.topicItem!.name!,
response.topicItem!.name,
style: const TextStyle(
fontSize: 16,
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,
children: [
text,
Padding(
padding: const EdgeInsets.only(left: 14),
child: SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed(
'/searchTrending',
parameters: {'tag': _tag},
),
label: Text(
'完整榜单',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
const SizedBox(width: 14),
SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed(
'/searchTrending',
parameters: {'tag': _tag},
),
label: Text(
'完整榜单',
style: TextStyle(
fontSize: 13,
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,

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_detail/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/fav/view.dart';
import 'package:PiliPlus/pages/fav_create/view.dart';
@@ -177,6 +178,7 @@ class Routes {
CustomGetPage(name: '/barSetting', page: () => const BarSetPage()),
CustomGetPage(name: '/upowerRank', page: () => const UpowerRankPage()),
CustomGetPage(name: '/spaceSetting', page: () => const SpaceSettingPage()),
CustomGetPage(name: '/dynTopicRcmd', page: () => const DynTopicRcmdPage()),
];
}