初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -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 竖图)— 否则横向 cell191:174
/// late finaloriginModel 不会变,避免 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;
// 必须用 videoArrVideoSimpleCell 只认 isAdsArr()adsInfoArr);
// 用 .video 塞的是 randomAdsInfocell 不识别会渲染成空白视频(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,
),
),
);
}
}