初始化

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,56 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../hj_model/cartoon_media_info.dart';
import '../../../hj_model/home/plate_model.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../hj_model/video_model.dart';
import '../../main_page/provider/bottom_bar_provider.dart';
/// 首页各 tab 逻辑基类:分页状态 + 列表数据 + 滚动/刷新控制器
/// 子类 HomeTabSortLogic(排序 tab/ HomeTabSectionLogic(专题 tab)复用
class HomeTabBaseLogic extends GetxController {
// ===== 外部传入 =====
final ModuleData tabModel; // tab 配置
final int index; // tab 下标
final bool isDarkStyle; // 是否暗网风格
HomeTabBaseLogic(this.index, this.tabModel, {this.isDarkStyle = false});
// ===== 状态 =====
int currentPage = 1;
bool isLoadingData = true;
// ===== 数据 =====
ModuleDetailModel? dataSource;
// ===== Controller =====
// scrollCtr 注册进单例 BottomProvider(持引用 + 加监听 + scrollToTop),生命周期比本类长;
// 本类不 dispose,否则单例再操作已释放的控制器会崩(交给单例托管,谁最终使用谁负责)
final ScrollController scrollCtr = ScrollController();
// refreshCtr 由页面 pullYsRefresh 的 onInit 注入、CustomRefreshView 负责释放,本类只持引用
RefreshController? refreshCtr;
// ===== 派生 getter =====
bool get isShowAd => tabModel.pureVersion != true; // 纯净版不展示广告
List<CartoonMediaInfo> get allMedia => dataSource?.allMediaInfo ?? [];
List<VideoModel> get allVideo => dataSource?.allVideoInfo ?? [];
List<AllSection> get allSection => dataSource?.allSection ?? [];
List<VideoModel> get chosenVideoInfo => dataSource?.chosenVideoInfo ?? []; // 精选视频
@override
void onInit() {
super.onInit();
_registerScrollController();
}
// 把本 tab 的滚动控制器注册到底部栏 Provider,联动「回到顶部」按钮与吸顶态
void _registerScrollController() {
if (isDarkStyle) {
DarkwebBottomProvider().setScrollController(index, controller: scrollCtr);
} else {
HomeBottomProvider().setScrollController(index, controller: scrollCtr);
}
}
}
@@ -0,0 +1,136 @@
import 'package:hgdj/tools_base/ad_manager.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../hj_utils/api_service/vid_service.dart';
import '../../../tools_base/debug_log.dart';
import '../../../tools_base/global_store/store.dart';
import 'home_tab_base_logic.dart';
class HomeTabSectionLogic extends HomeTabBaseLogic {
HomeTabSectionLogic(super.index, super.tabModel, {super.isDarkStyle});
bool lastIsGuessLike = false; // true 最后一个是猜你喜欢
//猜你喜欢当前选中的 tab **下标**,与 GuessLikeSliver 的标题一一对应:
//0 最多收藏 / 1 最新上架 / 2 最多观看
int guessLikeSortType = 0;
//上面的下标 → 后端 sort 值:最多收藏=0 最新上架=1 最多观看=3。
//标题在 GuessLikeSliver 里、值在这里,两处顺序必须一致,加减一项就会整体错位
List<int> gLikeSortParam = [0, 1, 3];
int guessLikePage = 1;
@override
void onReady() {
initData();
super.onReady();
}
void sortGuessLikeExchange(int value) {
if (value == guessLikeSortType) {
return;
}
if (lastIsGuessLike) {
allSection.last.allVideoInfo = null;
}
guessLikeSortType = value;
update();
_loadGuessLike(page: 1, sortType: guessLikeSortType);
}
void initData() async {
isLoadingData = true;
update();
_loadData();
}
void refreshData() {
_loadData();
}
void loadMoreData() {
if (lastIsGuessLike) {
_loadGuessLike(page: guessLikePage + 1, sortType: guessLikeSortType);
} else {
_loadData(page: currentPage + 1);
}
}
void _loadData({int page = 1, int size = 5}) async {
try {
ModuleDetailModel? retResp = await VidService.getModuleDetail(
tabModel.id ?? '',
pageNumber: page,
pageSize: size);
currentPage = page;
retResp?.allSection
?.removeWhere((element) => element.allVideoInfo?.isEmpty ?? true);
if (currentPage == 1) {
dataSource = retResp;
} else {
dataSource?.allSection?.addAll(retResp?.allSection ?? []);
}
//ab测试
for (AllSection section in (dataSource?.allSection ?? [])) {
globalStore.filterShowType(section.allVideoInfo ?? []);
}
if (isShowAd) {
AdManager().insertSectionAds(
dataSource?.allSection ?? [],
AdManager().adsByType(5),
adGap: 2,
);
}
if (retResp?.hasNext == true) {
lastIsGuessLike = false;
refreshCtr?.loadComplete();
} else {
if (retResp?.allSection?.last.isGuessLike == true) {
lastIsGuessLike = true;
guessLikeSortType = 0;
guessLikePage = 1;
if (dataSource?.allSection?.last.isAdsArr() == true) {
dataSource?.allSection?.removeLast();
}
retResp?.allSection?.last.allVideoInfo = null;
update();
await _loadGuessLike();
} else {
lastIsGuessLike = false;
refreshCtr?.loadNoData();
}
}
} catch (e) {
debugLog(e);
refreshCtr?.loadComplete();
}
refreshCtr?.refreshCompleted();
isLoadingData = false;
update();
}
Future _loadGuessLike({int page = 1, int size = 10, int sortType = 0}) async {
try {
AllSection lastSection = allSection.last;
int sortValue = gLikeSortParam[sortType];
AllSection? retModel = await VidService.getGuessLike(
page, size, tabModel.id ?? "", sortValue);
if (sortType != guessLikeSortType) {
return;
}
guessLikePage = page;
lastSection.allVideoInfo ??= [];
if (lastSection.allVideoInfo?.isNotEmpty == true && page == 1) {
lastSection.allVideoInfo?.clear();
}
lastSection.allVideoInfo?.addAll(retModel?.allVideoInfo ?? []);
retModel?.hasNext == false
? refreshCtr?.loadNoData()
: refreshCtr?.loadComplete();
} catch (e) {
refreshCtr?.loadComplete();
}
refreshCtr?.refreshCompleted();
update();
}
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import '../../../hj_model/home/plate_model.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../tools_base/banner/ads_grid_view_widget.dart';
import '../../../tools_base/refresh/pull_refresh.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import '../home_cell_style/guess_like_sliver.dart';
import '../home_cell_style/quick_entry_row.dart';
import '../home_cell_style/home_section_cell.dart';
import 'home_tab_section_logic.dart';
/// 海角样式
class HomeTabSectionPage extends StatefulWidget {
final ModuleData tabModel;
final bool isDarkStyle;
final int tabIndex;
const HomeTabSectionPage(
this.tabIndex,
this.tabModel, {
super.key,
this.isDarkStyle = false,
});
@override
State<HomeTabSectionPage> createState() => _HomeTabSectionPageState();
}
class _HomeTabSectionPageState extends State<HomeTabSectionPage>
with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<HomeTabSectionLogic>(
init: HomeTabSectionLogic(widget.tabIndex, widget.tabModel,
isDarkStyle: widget.isDarkStyle),
tag: uniqueTag,
builder: (logic) {
return Container(
child: pullYsRefresh(
onInit: (ctr) => logic.refreshCtr = ctr,
onLoading: (ctr) => logic.loadMoreData(),
onRefresh: (ctr) => logic.refreshData(),
child: CustomScrollView(
controller: logic.scrollCtr,
slivers: [
if (logic.isShowAd)
SliverToBoxAdapter(
child: AdsGridViewWidget(
4,
accordingAdsType: true,
padding: EdgeInsets.fromLTRB(12, 0, 12, 12),
),
),
SliverToBoxAdapter(child: QuickEntryRow(widget.tabModel)),
if (logic.isLoadingData)
SliverToBoxAdapter(
child: Container(
height: 300,
child: LoadingCenterWidget(),
),
)
else if (logic.allSection.isEmpty)
SliverToBoxAdapter(
child: Container(
height: 300,
child: CErrorWidget(
retryOnTap: () => logic.initData(),
),
),
)
else
..._buildContent(logic),
],
),
),
);
},
);
}
List<Widget> _buildContent(HomeTabSectionLogic logic) {
return [
SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverList.builder(
itemBuilder: (BuildContext context, int index) {
AllSection section = logic.allSection[index];
if (section.isAdsArr()) {
return AdsGridViewWidget(
-1,
adsArr: section.adsInfoArr ?? [],
padding: EdgeInsets.fromLTRB(12, 0, 12, 16),
);
} else if (logic.lastIsGuessLike &&
index == logic.allSection.length - 1) {
return SizedBox(); // 最后一个猜你喜欢列表单独处理
} else if (section.allVideoInfo?.isNotEmpty == true) {
//换一批、间距、分割线都在 HomeSectionCell 里
return HomeSectionCell(section);
} else {
return SizedBox();
}
},
itemCount: logic.allSection.length,
),
if (logic.lastIsGuessLike)
GuessLikeSliver(
logic.allSection.last,
sortIndex: logic.guessLikeSortType,
logic: logic,
),
];
}
}
@@ -0,0 +1,186 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_model/home/video_list_model.dart';
import 'package:hgdj/hj_page/home/provider/home_update_marker_provider.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:uuid/uuid.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../hj_utils/api_service/vid_service.dart';
import '../../../tools_base/ad_manager.dart';
import '../../../tools_base/debug_log.dart';
import '../../../tools_base/global_store/store.dart';
import 'home_tab_base_logic.dart';
const Interval = 4;
class HomeTabSortLogic extends HomeTabBaseLogic {
HomeTabSortLogic(super.index, super.tabModel, {super.isDarkStyle});
//普通排序:后端没配就用这套默认
List<SortTab<int>> get _sortTabs =>
tabModel.sortTabs ??
const [
SortTab('热门推荐', 2),
SortTab('最新上架', 1),
SortTab('最新热评', 9),
SortTab('最多收藏', 7),
];
List<String> get sortTitles => _sortTabs.map((e) => e.name).toList();
// 初值跟第一个 tab 走:HomeSortHeader 的 TabController 默认选中 index 0,而后端可能用 top 把别的排序项置顶,
// 写死 2 就会出现「高亮第一个 tab、请求发的却是本月最热」的错位(连带影响精选视频展示、随机刷新判断)
late int moduleSort = _sortTabs.first.sort; // 1: 最新发布 2:本月最热 9:最新热评 7:最多收藏
//"最新"风格固定排序
static const _lastestSortTabs = [
SortTab('今日最新', 1),
SortTab('本周最热', 2),
SortTab('本月最热', 4),
SortTab('年度最热', 5),
];
List<String> get lastestSortTitles =>
_lastestSortTabs.map((e) => e.name).toList();
int lastestModuleSort = 1;
// 一排几个:haiJiaoStyle.defaultShow 0-一排两个(网格) 1-一排一个(单列),缺省按网格
late final RxBool isGridStyle = (tabModel.haiJiaoStyle?.defaultShow != 1).obs;
RxBool isSortMenuInTop = false.obs;
bool get isNewestStyle => tabModel.id == '-110';
// 请求序号:每发一次 +1,响应回来时序号已变说明期间切了排序 / 又发了新请求,旧响应直接丢弃。
// 否则先发后回的结果会盖掉新排序的列表,或把分页偏移打乱(如上一页的 addAll 追加到刷新后的列表尾部)
int _reqSeq = 0;
// 当前排序项是否走随机刷新接口:只认后端下发的 refreshMode,不匹配标题、不硬编码 sort 值
// 暗网亚模块产品上就不做随机,别删这个判断——后端对没配 refreshMode 的历史数据会把
// val=2 兜底成 RANDOM_TOP_N,只靠后端配置挡不住
bool get _isRandomRefresh {
if (isNewestStyle || isDarkStyle) return false;
return tabModel.sortRuleOf(moduleSort)?.isRandomRefresh == true;
}
void initData() async {
isLoadingData = true;
update();
_loadData(sortValue: isNewestStyle ? lastestModuleSort : moduleSort);
}
@override
void onReady() {
initData();
super.onReady();
}
void sortMenuEvent(int value) {
if (isNewestStyle) {
lastestModuleSort = _lastestSortTabs[value].sort;
if (lastestModuleSort == 1) {
HomeUpdateMarkerProvider().markTodayLatestViewed();
}
} else {
moduleSort = _sortTabs[value].sort;
}
isLoadingData = true;
update();
_loadData(sortValue: isNewestStyle ? lastestModuleSort : moduleSort);
}
void onTooleAction() {
isGridStyle.value = !isGridStyle.value;
update();
}
void refreshData() {
// 随机刷新排序项每次主动下拉都换新 token,后端据此重排候选池;
// 网络层自动重试沿用同一 URL(token 不变),拿到的顺序与首次一致,不会跳序
_loadData(
sortValue: isNewestStyle ? lastestModuleSort : moduleSort,
refreshToken: _isRandomRefresh ? const Uuid().v4() : null,
);
}
void loadMoreData() {
_loadData(
page: currentPage + 1,
sortValue: isNewestStyle ? lastestModuleSort : moduleSort);
}
// 每页固定 30:随机刷新接口也传同一个 size,分页偏移才对齐不重叠
// [refreshToken] 非空 = 走热门随机刷新接口,只换视频列表
void _loadData(
{int page = 1,
int size = 30,
int sortValue = 0,
String? refreshToken}) async {
final seq = ++_reqSeq;
try {
bool hasNext = false;
if (isNewestStyle) {
VideoListResp? retResp = await VidService.getNewestModule(
sortType: sortValue,
pageNumber: page,
pageSize: size,
);
if (seq != _reqSeq) return; // 已被更新的请求取代,本次结果作废
currentPage = page;
if (currentPage == 1) {
dataSource = ModuleDetailModel(allVideoInfo: retResp?.videos ?? []);
} else {
dataSource?.allVideoInfo?.addAll(retResp?.videos ?? []);
}
hasNext = retResp?.hasNext ?? false;
} else if (refreshToken != null && dataSource != null) {
// 热门随机刷新:接口只回 allVideoInfo,专题/精选/漫画等结构沿用上次结果,不能整个换掉 dataSource
// 首屏还没成功过(dataSource 为空)时不走这里,让它走下面的常规接口拿全量结构
final retResp = await VidService.refreshRandomModule(
tabModel.id ?? '',
moduleSort: sortValue,
refreshToken: refreshToken,
pageSize: size,
);
if (seq != _reqSeq) return; // 已被更新的请求取代,本次结果作废
// 刷新接口的 hasNext 恒为 true(随机只换第一页,到底没到底由后续常规分页说了算),取不到按 true 兜底
hasNext = retResp?.hasNext ?? true;
final list = retResp?.allVideoInfo;
// 请求失败或响应没带 allVideoInfo 都算这次刷新没成:列表和页码原样留着,
// 别把用户正看着的内容清空、也别把分页偏移打乱(后端真没内容会回空数组,那才该清)
if (list != null) {
dataSource?.allVideoInfo = list;
currentPage = 1; // 刷新成功才重置页码,下次上拉从 pageNumber=2 开始
}
} else {
ModuleDetailModel? retResp = await VidService.getModuleDetail(
tabModel.id ?? '',
moduleSort: sortValue,
pageNumber: page,
pageSize: size);
if (seq != _reqSeq) return; // 已被更新的请求取代,本次结果作废
currentPage = page;
if (currentPage == 1) {
dataSource = retResp;
} else {
dataSource?.allVideoInfo?.addAll(retResp?.allVideoInfo ?? []);
dataSource?.allMediaInfo?.addAll(retResp?.allMediaInfo ?? []);
}
hasNext = retResp?.hasNext ?? false;
}
//ab测试
globalStore.filterShowType(dataSource?.allVideoInfo ?? []);
dataSource?.allVideoInfo?.removeWhere((element) => element.isRandomAd());
if (isShowAd) {
AdManager().insertGroupAds(
dataSource?.allVideoInfo ?? [], AdManager().adsByType(11),
adGap: 6);
}
hasNext ? refreshCtr?.loadComplete() : refreshCtr?.loadNoData();
} catch (e) {
debugLog(e);
if (seq != _reqSeq) return; // 旧请求失败别去动刷新态,新请求还在跑
refreshCtr?.loadComplete();
}
refreshCtr?.refreshCompleted();
isLoadingData = false;
update();
}
}
@@ -0,0 +1,316 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import '../../../hj_model/cartoon_media_info.dart';
import '../../../hj_model/home/plate_model.dart';
import '../../../hj_model/video_model.dart';
import '../../../hj_utils/sliver_delegate.dart';
import '../../../tools_base/banner/ads_grid_view_widget.dart';
import '../../../tools_base/refresh/pull_refresh.dart';
import '../../cartoon/acg_widget_item.dart';
import '../home_cell_style/quick_entry_row.dart';
import '../home_cell_style/video_simple_cell.dart';
import 'home_tab_sort_logic.dart';
import 'widget/sort_header.dart';
import 'widget/special_topics_view.dart';
/// 海角样式
class HomeTabSortPage extends StatefulWidget {
final ModuleData tabModel;
final bool isDarkStyle;
final int tabIndex;
const HomeTabSortPage(
this.tabIndex,
this.tabModel, {
super.key,
this.isDarkStyle = false,
});
@override
State<HomeTabSortPage> createState() => _HomeTabSortPageState();
}
class _HomeTabSortPageState extends State<HomeTabSortPage> with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<HomeTabSortLogic>(
init: HomeTabSortLogic(widget.tabIndex, widget.tabModel,
isDarkStyle: widget.isDarkStyle),
tag: uniqueTag,
builder: (logic) {
return Stack(
children: [
Container(
child: pullYsRefresh(
onInit: (ctr) => logic.refreshCtr = ctr,
onLoading: (ctr) => logic.loadMoreData(),
onRefresh: (ctr) => logic.refreshData(),
child: CustomScrollView(
controller: logic.scrollCtr,
slivers: [
SliverToBoxAdapter(
child: 6.sizeBoxH,
),
if (logic.isShowAd && !logic.isNewestStyle)
SliverToBoxAdapter(
child: AdsGridViewWidget(
4,
accordingAdsType: true,
padding: EdgeInsets.fromLTRB(12, 0, 12, 12),
),
),
SliverToBoxAdapter(child: QuickEntryRow(widget.tabModel)),
SliverToBoxAdapter(
child: SpecialTopicsView(
logic.allSection,
module: widget.tabModel,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
isDarkStyle: widget.isDarkStyle,
),
),
if (widget.tabModel.haiJiaoStyle?.sortShow == 1 ||
logic.isNewestStyle)
SliverPersistentHeader(
pinned: true,
delegate: MySliverDelegate(
maxHeight: 42,
minHeight: 42,
callTop: (shrinkOffset) {
if (shrinkOffset > 0) {
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
logic.isSortMenuInTop.value = false;
});
}
if (shrinkOffset <= 0) {
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
logic.isSortMenuInTop.value = true;
});
}
},
child: Obx(
() {
Color? bgColor;
if (widget.isDarkStyle) {
if (logic.isSortMenuInTop.value) {
bgColor = Colors.transparent;
} else {
bgColor = Colors.black;
}
}
return HomeSortHeader(
isLatest: logic.isNewestStyle,
labelPadding:
const EdgeInsets.symmetric(horizontal: 10),
tabs: logic.isNewestStyle
? logic.lastestSortTitles
: logic.sortTitles,
bgColor: bgColor,
onSort: logic.sortMenuEvent,
rightWidget: logic.isNewestStyle
? null
: _buildSortStyleMenu(logic),
);
},
),
),
),
_buildContent(logic),
],
),
),
),
],
);
},
);
}
Widget _buildSortStyleMenu(HomeTabSortLogic logic) {
if (widget.tabModel.isACG) return SizedBox();
return Container(
padding: EdgeInsets.only(right: 12),
child: InkWell(
enableFeedback: false,
onTap: logic.onTooleAction,
child: Row(
children: [
Text(
'切换',
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: .6),
),
),
2.sizeBoxW,
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, anim) =>
ScaleTransition(scale: anim, child: child),
child: Image.asset(
logic.isGridStyle.value
? 'list_style.webp'.homePath
: 'grid_style.webp'.homePath,
key: ValueKey(logic.isGridStyle.value),
width: 18,
),
),
],
),
),
);
}
Widget _buildContent(HomeTabSortLogic logic) {
if (logic.isLoadingData) {
return SliverFillRemaining(
child: LoadingCenterWidget(),
);
} else if (logic.allVideo.isEmpty && logic.allMedia.isEmpty) {
return SliverFillRemaining(
child: CErrorWidget(
retryOnTap: () => logic.initData(),
),
);
} else {
if (widget.tabModel.isACG) {
return SliverPadding(
padding: EdgeInsets.fromLTRB(16, 10, 16, 0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 5,
childAspectRatio: 111 / 194, //子控件宽高比
),
delegate: SliverChildBuilderDelegate(
(context, index) {
CartoonMediaInfo info = logic.allMedia[index];
return AcgItemWidget(info: info);
},
childCount: logic.allMedia.length,
),
),
);
} else {
if (widget.tabModel.haiJiaoStyle?.showChosenVideo == 1 &&
logic.chosenVideoInfo.isNotEmpty &&
logic.moduleSort == 2) {
//热门推荐,每个6个插入一个精品视频数据
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
sliver: SliverMainAxisGroup(
slivers: _buildChosenVideoStyle(logic),
),
);
} else {
return SliverPadding(
padding: EdgeInsets.fromLTRB(16, 10, 16, 0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: logic.isGridStyle.value ? 2 : 1,
mainAxisSpacing: logic.isGridStyle.value ? 12 : 14,
crossAxisSpacing: 7,
childAspectRatio:
logic.isGridStyle.value ? 168 / 164 : 340 / 232, //子控件宽高比
),
delegate: SliverChildBuilderDelegate(
(context, index) {
VideoModel videoModel = logic.allVideo[index];
return GestureDetector(
onTap: () async {
pushToVideoPage(videoModel: videoModel);
},
child: VideoSimpleCell(
videoModel: videoModel,
textLines: logic.isGridStyle.value ? 2 : 1,
),
);
},
childCount: logic.allVideo.length,
),
),
);
}
}
}
}
List<Widget> _buildChosenVideoStyle(HomeTabSortLogic logic) {
List<Widget> slivers = [];
int gapValue = 6;
int allIndex = logic.allVideo.length ~/ gapValue;
int leftCount = logic.allVideo.length % gapValue;
int insertIndex = 0;
for (int i = 0; i < allIndex; i++) {
List<VideoModel> subList =
logic.allVideo.sublist(i * gapValue, i * gapValue + gapValue);
slivers.add(SliverPadding(
padding: EdgeInsets.only(bottom: 12),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: logic.isGridStyle.value ? 2 : 1,
mainAxisSpacing: logic.isGridStyle.value ? 10 : 12,
crossAxisSpacing: 7,
childAspectRatio:
logic.isGridStyle.value ? 168 / 144 : 343 / 260, //子控件宽高比
),
delegate: SliverChildBuilderDelegate(
(context, index) {
VideoModel videoModel = subList[index];
return VideoSimpleCell(
videoModel: videoModel,
);
},
childCount: subList.length,
),
),
));
if (insertIndex < logic.chosenVideoInfo.length) {
VideoModel insertVM = logic.chosenVideoInfo[insertIndex];
slivers.add(SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(bottom: 12),
child: AspectRatio(
aspectRatio: 343 / 260,
child: VideoSimpleCell(videoModel: insertVM),
),
),
));
insertIndex++;
}
}
if (leftCount > 0) {
List<VideoModel> subList = logic.allVideo.sublist(allIndex * gapValue);
slivers.add(SliverPadding(
padding: EdgeInsets.only(bottom: 12),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: logic.isGridStyle.value ? 2 : 1,
mainAxisSpacing: logic.isGridStyle.value ? 10 : 12,
crossAxisSpacing: 7,
childAspectRatio:
logic.isGridStyle.value ? 168 / 144 : 343 / 260, //子控件宽高比
),
delegate: SliverChildBuilderDelegate(
(context, index) {
VideoModel videoModel = subList[index];
return VideoSimpleCell(
videoModel: videoModel,
);
},
childCount: subList.length,
),
),
));
}
return slivers;
}
}
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_page/home/provider/home_update_marker_provider.dart';
import 'package:provider/provider.dart';
/// 首页模块的排序切换栏(热门推荐 / 最新上架 / ...)
/// [isLatest]:「最新」页专用紧凑样式——字号更小、选中态是红色胶囊块、首个 tab 带更新红点
class HomeSortHeader extends StatefulWidget {
final ValueChanged<int> onSort;
final List<String>? tabs;
final EdgeInsets? labelPadding;
final Widget? rightWidget;
final Color? bgColor;
final bool isLatest;
const HomeSortHeader({
super.key,
required this.onSort,
this.tabs,
this.labelPadding,
this.rightWidget,
this.bgColor,
this.isLatest = false,
});
@override
State<HomeSortHeader> createState() => _HomeSortHeaderState();
}
class _HomeSortHeaderState extends State<HomeSortHeader>
with SingleTickerProviderStateMixin {
late final List<String> tabs =
widget.tabs ?? const ['热门推荐', '最新上架', '最新热评', '最多收藏'];
late final TabController tabCtr =
TabController(length: tabs.length, vsync: this);
@override
void dispose() {
tabCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: 38,
color: widget.bgColor ?? Theme.of(context).scaffoldBackgroundColor,
child: widget.rightWidget == null
? _tabBar()
: Row(children: [Expanded(child: _tabBar()), widget.rightWidget!]),
);
}
Widget _tabBar() {
final fontSize = widget.isLatest ? 12.0 : 14.0;
return TabBar(
controller: tabCtr,
isScrollable: true,
tabAlignment: TabAlignment.center,
onTap: widget.onSort,
indicatorWeight: 0,
//最新页选中态是红色胶囊块,普通页无指示器(靠字号/字重区分)
indicator: widget.isLatest
? BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3))
: const BoxDecoration(),
labelPadding: widget.isLatest
? const EdgeInsets.symmetric(horizontal: 4)
: widget.labelPadding ?? const EdgeInsets.symmetric(horizontal: 8),
labelStyle: TextStyle(
color: const Color(0xE5FFFFFF),
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle:
TextStyle(color: const Color(0x73FFFFFF), fontSize: fontSize),
tabs: List.generate(tabs.length, (i) => _tab(i, tabs[i])),
);
}
/// 单个 tab。红点只可能落在「最新」页的首个 tab 上,
/// 所以只让它订阅 provider——其余 tab 不会因红点变化而重建
Widget _tab(int index, String label) {
if (!widget.isLatest) return Text(label);
final text = Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Text(label),
);
if (index != 0) return text;
return Consumer<HomeUpdateMarkerProvider>(
builder: (_, marker, __) => Stack(
clipBehavior: Clip.none,
children: [
text, // Stack 尺寸取它,有无红点 tab 宽高一致
if (marker.showTodayLatestDot)
Positioned(
right: 4,
top: 0,
child: HomeUpdateMarkerProvider.buildRedDot()),
],
),
);
}
}
@@ -0,0 +1,268 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_model/home/plate_model.dart';
import 'package:hgdj/hj_model/home/module_detail_model.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/refresh/horizontal_load_more.dart';
import '../../../../tools_base/widget/net_image_widget.dart';
import '../../../cartoon/cartoon_sectionlist_page.dart';
import '../../section_all_page/section_all_page.dart';
import '../../special_topic_detail/special_topics_detail_page.dart';
/// 首页/漫画页的专题位,样式由 [module].haiJiaoStyle.sectionStyle 决定:
/// 0-不展示 1-圆头像横滑(17岁) 2/3-原创达人(女优/网黄) 4-文字标签网格 5-带标题的圆头像横滑(图列)
class SpecialTopicsView extends StatelessWidget {
final List<AllSection>? specials;
final ModuleData? module;
final EdgeInsetsGeometry? padding;
final bool isDarkStyle;
const SpecialTopicsView(
this.specials, {
super.key,
this.module,
this.padding,
this.isDarkStyle = false,
});
int get _style => module?.haiJiaoStyle?.sectionStyle ?? 0;
@override
Widget build(BuildContext context) {
final list = specials;
if (list == null || list.isEmpty) return const SizedBox.shrink();
switch (_style) {
case 1:
return _avatarRow(list, height: 88, imgSize: 60);
case 2:
case 3:
return _actressSection(list);
case 4:
return _tagGrid(list);
case 5:
return _imageSection(list);
default:
return const SizedBox.shrink();
}
}
/// 进「原创达人」全部列表页。标题右侧的「更多」和横滑到底松手都走这里
void _gotoSectionAll() => Get.to(SectionAllPage(module?.id ?? ""));
/// 专题点击:漫画模块进漫画专区列表,其余进专题详情
/// 注:原创达人区不走这里,它固定进专题详情(见 [_actressSection]
void _onTap(AllSection model) {
if (module?.isACG == true) {
Get.to(CartoonSectionListPage(
sectionID: model.sectionID, tagName: model.sectionName));
} else {
Get.to(SpecialTopicsDetailPage(model), preventDuplicates: false);
}
}
// ========== 样式 1 / 5:圆头像横滑 ==========
Widget _avatarRow(List<AllSection> list,
{required double height, required double imgSize}) {
return Container(
margin: padding,
height: height,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: list.length,
itemBuilder: (_, i) => _avatarItem(list[i], imgSize),
),
);
}
Widget _avatarItem(AllSection model, double imgSize) {
return GestureDetector(
onTap: () => _onTap(model),
child: Container(
width: 64,
margin: const EdgeInsets.only(right: 16),
child: Column(
children: [
NetworkImageLoader(
imageUrl: model.sectionCover ?? '',
width: imgSize,
height: imgSize,
borderRadius: imgSize / 2,
),
4.sizeBoxH,
Text(
model.sectionName ?? '',
style: const TextStyle(color: Color(0xE5FFFFFF), fontSize: 12),
maxLines: 1,
),
],
),
),
);
}
/// 样式 5:标题 + 圆头像横滑。标题固定取第一个专题名
/// 注意标题不套 [padding](贴左边缘),只有下方列表有边距——与设计稿一致,别顺手加上
Widget _imageSection(List<AllSection> list) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
list.first.sectionName ?? "",
maxLines: 1,
style: const TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 18,
fontWeight: FontWeight.w500),
),
_avatarRow(list, height: 84, imgSize: 55),
],
);
}
// ========== 样式 4:文字标签网格 ==========
Widget _tagGrid(List<AllSection> list) {
return GridView.builder(
shrinkWrap: true,
padding: padding,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 10,
crossAxisSpacing: 9,
childAspectRatio: 82 / 33,
),
itemCount: list.length,
itemBuilder: (_, i) => _tagItem(list[i]),
);
}
Widget _tagItem(AllSection model) {
return GestureDetector(
onTap: () => _onTap(model),
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color:
isDarkStyle ? const Color(0x4D810906) : const Color(0x0DFFFFFF),
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
child: Stack(
alignment: Alignment.center,
children: [
if (isDarkStyle)
Positioned(
top: 0,
child: Image.asset("aw_tag.webp".homePath, height: 13)),
Text(
"${model.sectionName}",
style: const TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 12,
fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (model.hot == true)
Positioned(
top: 0,
right: 0,
child: Image.asset("home_hot.webp".homePath, height: 20)),
],
),
),
);
}
// ========== 样式 2 / 3:原创达人 ==========
Widget _actressSection(List<AllSection> list) {
return Container(
margin: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
enableFeedback: false,
onTap: _gotoSectionAll,
child: Row(
children: [
const Expanded(
child: Text(
'原创达人',
maxLines: 1,
style: TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 18,
fontWeight: FontWeight.w500),
),
),
10.sizeBoxW,
const Text('更多',
maxLines: 1,
style: TextStyle(color: Color(0x59FFFFFF), fontSize: 12)),
Image.asset("arrow_right_grey.webp".commonImgPath, width: 18),
],
),
),
12.sizeBoxH,
// 一屏排 5.5 个(露出半个提示可滑)+ 5 个 12 的间隔;
// 高度 = 圆头像(正方形,边长即 item 宽) + 2 间距 + 20 文字
LayoutBuilder(
builder: (_, c) {
final itemW = (c.maxWidth - 12 * 5) / 5.5;
return SizedBox(
height: itemW + 22,
// 横向「拉到底查看更多」:滑到末尾继续拉、松手跳原创达人列表页
child: HorizontalLoadMore(
onTrigger: _gotoSectionAll,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
itemCount: list.length,
separatorBuilder: (_, __) => 12.sizeBoxW,
itemBuilder: (_, i) => _actressItem(list[i], itemW),
),
),
);
},
),
],
),
);
}
Widget _actressItem(AllSection model, double itemW) {
return GestureDetector(
//达人固定进专题详情,不走 _onTap 的漫画分支
onTap: () =>
Get.to(SpecialTopicsDetailPage(model), preventDuplicates: false),
child: SizedBox(
height: double.infinity,
width: itemW,
child: Column(
children: [
NetworkImageLoader(
imageUrl: model.sectionCover ?? '',
width: itemW,
height: itemW,
borderRadius: itemW,
),
2.sizeBoxH,
Text(
model.sectionName ?? '',
style: const TextStyle(color: Color(0xE5FFFFFF), fontSize: 12),
maxLines: 1,
),
],
),
),
);
}
}