初始化
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user