初始化
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/home/module_detail_model.dart';
|
||||
import 'package:hgdj/hj_model/splash/ads_model.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
import 'package:hgdj/tools_base/ad_manager.dart';
|
||||
|
||||
import '../home_cell_style/home_section_cell.dart';
|
||||
|
||||
/// 分页/刷新/加载态/防重入/异常兜底都交给 ListBaseLogic,本类只管排序切换与广告插入
|
||||
class SpecialTopicsLogic extends ListBaseLogic<VideoModel> {
|
||||
final AllSection? originModel;
|
||||
SpecialTopicsLogic(this.originModel);
|
||||
|
||||
// ========== 配置 ==========
|
||||
static const _sortTabs = [
|
||||
SortTab('热门推荐', 'like'),
|
||||
SortTab('最新上架', 'new'),
|
||||
SortTab('最新热评', 'comment'),
|
||||
];
|
||||
|
||||
/// 走竖图布局的 showType(后端 2xx 一档)
|
||||
static const _verticalTypes = {
|
||||
HomeSectionShowType.verticalScroll25,
|
||||
HomeSectionShowType.verticalFourGrid,
|
||||
HomeSectionShowType.verticalSixGrid,
|
||||
HomeSectionShowType.verticalNineGrid,
|
||||
HomeSectionShowType.verticalListGrid,
|
||||
};
|
||||
|
||||
List<String> get sortTitles => _sortTabs.map((e) => e.name).toList();
|
||||
|
||||
// ========== 状态 ==========
|
||||
int sort = 0;
|
||||
|
||||
/// 广告只取一次,之后每页复用(只表示「取过」,取到空也算取过)
|
||||
bool _adsLoaded = false;
|
||||
final List<AdsInfoModel> _adsList = [];
|
||||
|
||||
// ========== Controllers ==========
|
||||
late final TabController tabCtr =
|
||||
TabController(length: _sortTabs.length, vsync: this);
|
||||
|
||||
// ========== 派生 getter ==========
|
||||
String get title => originModel?.sectionName ?? '';
|
||||
|
||||
/// 竖向 cell 布局(封面 168:266 竖图)— 否则横向 cell(191:174)
|
||||
/// late final:originModel 不会变,避免 textLines 在 itemBuilder 里被每个 cell 触发一次线性查找
|
||||
late final bool isVertical = _verticalTypes.contains(
|
||||
HomeSectionShowType.values.firstWhere(
|
||||
(e) => e.value == originModel?.showType,
|
||||
orElse: () => HomeSectionShowType.oneLargeAndFourSmall,
|
||||
),
|
||||
);
|
||||
|
||||
int get crossAxisCount => isVertical ? 3 : 2;
|
||||
double get childAspectRatio => isVertical ? (168 / 266) : (191 / 174);
|
||||
int get textLines => isVertical ? 1 : 2;
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ========== 公开方法 ==========
|
||||
/// [isRefresh] 刷新(回第 1 页并清空)/ 加载更多;[showLoading] 切排序时先转圈
|
||||
/// 广告要等基类把新页并入 dataList 后再按全量列表重排,所以 await 完再插
|
||||
Future<void> loadData(
|
||||
{bool isRefresh = true, bool showLoading = false}) async {
|
||||
if (showLoading) {
|
||||
dataList = null; // 置空即 isLoading
|
||||
update();
|
||||
}
|
||||
await fetchData(isRefresh: isRefresh, fetch: _fetch);
|
||||
_insertAds();
|
||||
update();
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
Future<(List<VideoModel>?, bool)> _fetch(int page) async {
|
||||
final res = await VidService.fetchSectionVideos(
|
||||
originModel?.sectionID ?? '',
|
||||
pageNumber: page,
|
||||
sortType: _sortTabs[sort].sort,
|
||||
);
|
||||
// hasNext 为 null 按「没有更多」处理,保持原 == true 的判断语义
|
||||
return (res?.videos, res?.hasNext == true);
|
||||
}
|
||||
|
||||
/// 往列表里插广告。AdManager().adsByType 是纯内存读、无网络请求,
|
||||
/// 整条链同步即可。insertGroupAds 内部先清旧广告位再重排,全量列表上重复调用是安全的。
|
||||
void _insertAds() {
|
||||
if (!_adsLoaded) {
|
||||
_adsList
|
||||
..clear()
|
||||
..addAll(AdManager().adsByType(8));
|
||||
_adsLoaded = true;
|
||||
}
|
||||
final list = dataList;
|
||||
if (_adsList.isEmpty || list == null) return;
|
||||
// 必须用 videoArr:VideoSimpleCell 只认 isAdsArr()(adsInfoArr);
|
||||
// 用 .video 塞的是 randomAdsInfo,cell 不识别会渲染成空白视频(91PORN 占位 + 评论 null)
|
||||
AdManager().insertGroupAds(list, _adsList, adGap: 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../../hj_model/home/module_detail_model.dart';
|
||||
import '../home_cell_style/video_simple_cell.dart';
|
||||
import 'special_topics_detail_logic.dart';
|
||||
|
||||
class SpecialTopicsDetailPage extends StatelessWidget {
|
||||
final AllSection? originModel;
|
||||
|
||||
const SpecialTopicsDetailPage(this.originModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<SpecialTopicsLogic>(
|
||||
init: SpecialTopicsLogic(originModel),
|
||||
builder: (logic) => Scaffold(
|
||||
appBar: AppBar(title: Text(logic.title)),
|
||||
body: Column(
|
||||
children: [
|
||||
_sortBar(logic),
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
onRefresh: (_) => logic.loadData(),
|
||||
onLoading: (_) => logic.loadData(isRefresh: false),
|
||||
child: _content(logic),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sortBar(SpecialTopicsLogic logic) {
|
||||
final titles = logic.sortTitles;
|
||||
return Container(
|
||||
height: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 32, vertical: 6),
|
||||
child: TabBar(
|
||||
controller: logic.tabCtr,
|
||||
tabAlignment: TabAlignment.fill,
|
||||
isScrollable: false,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
labelStyle: const TextStyle(fontSize: 14),
|
||||
labelColor: const Color(0xE5FFFFFF),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 14),
|
||||
unselectedLabelColor: const Color(0x73FFFFFF),
|
||||
indicator: const BoxDecoration(),
|
||||
onTap: (index) {
|
||||
logic.sort = index;
|
||||
logic.loadData(showLoading: true);
|
||||
},
|
||||
tabs: [
|
||||
for (var i = 0; i < titles.length; i++)
|
||||
_sortTab(titles[i], isLast: i == titles.length - 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sortTab(String title, {required bool isLast}) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: isLast ? Colors.transparent : const Color(0x12FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(title),
|
||||
);
|
||||
}
|
||||
|
||||
/// loading / 空态 / 视频网格
|
||||
Widget _content(SpecialTopicsLogic logic) {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.isEmptyData) return const CErrorWidget();
|
||||
final list = logic.dataList!; // 上面两个判空已保证非空
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: logic.crossAxisCount,
|
||||
crossAxisSpacing: 7,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: logic.childAspectRatio,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) => SizedBox.expand(
|
||||
child: VideoSimpleCell(
|
||||
videoModel: list[index],
|
||||
textLines: logic.textLines,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user