Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_infinite_marquee/flutter_infinite_marquee.dart';
import 'package:jydj/hj_utils/screen.dart';
import 'package:jydj/tools_base/ad_manager.dart';
import '../../hj_model/splash/ads_model.dart';
import '../../track_event_manager/track_manager.dart';
import 'ads_banner.dart';
import 'ads_item.dart';
/// 广告位展示样式,由广告的 coverImgSize 映射而来(见 AdsInfoModel.adStyle)
enum AdStyle {
normal, //两排共 10 个(固定样式时用)
banner, //大 banner 轮播
twoAutoScroll, //双排:上排固定,下排自动滚动
oneScrollBig, //单排自动滚动的大图
}
/// 广告位通用容器:按 [AdStyle] 切换九宫格 / banner / 双排滚动 / 大图滚动
class AdsGridView extends StatelessWidget {
final int position; //广告位标识,ads 为空时按此取本地缓存广告
final List<AdsInfoModel>? ads; //外部直接传入的广告,优先于 position
final bool autoStyle; //true:按第一个广告的 coverImgSize 选样式;false:固定两排九宫格
final EdgeInsetsGeometry? padding;
final AdExposureModel? exposureModel; //曝光埋点模型
const AdsGridView(
this.position, {
super.key,
this.ads,
this.autoStyle = false,
this.padding,
this.exposureModel,
});
@override
Widget build(BuildContext context) {
//一次 build 只取一次,自动滚动的 itemBuilder 复用同一份,避免反复全表 filter
final list = ads ?? AdManager().adsByType(position);
//无数据或 ab 测未命中广告,直接占位不渲染
if (list.isEmpty || !AdManager().showAbTestAd) return const SizedBox();
final style = autoStyle ? list.first.adStyle : AdStyle.normal;
return switch (style) {
AdStyle.normal => _normal(list),
AdStyle.banner => _banner(list),
AdStyle.twoAutoScroll => _twoAutoScroll(list),
AdStyle.oneScrollBig => _oneScrollBig(list),
};
}
//统一构造广告项;每个 item 用独立的曝光模型副本,避免共享同一实例导致曝光 adType 被覆盖串号
Widget _item(AdsInfoModel model, {double? aspectRatio}) {
final em = exposureModel;
return AdsItem(
adInfo: model,
aspectRatio: aspectRatio,
exposureModel: em == null ? null : AdExposureModel(pageType: em.pageType, adType: model.adType, tabIndex: em.tabIndex),
);
}
//两排共 10 个
Widget _normal(List<AdsInfoModel> list) {
return GridView.builder(
padding: padding,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 12,
crossAxisSpacing: 10,
childAspectRatio: 60 / 82,
),
itemCount: min(10, list.length),
itemBuilder: (_, index) => _item(list[index]),
);
}
//大 banner:单张轮播,最多 20 张
Widget _banner(List<AdsInfoModel> list) {
return Container(
padding: padding,
alignment: Alignment.center,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: AspectRatio(
aspectRatio: 720 / 150,
child: AdsBanner(
list.length > 20 ? list.take(20).toList() : list,
autoPlayMs: 2000,
exposureModel: exposureModel,
),
),
),
);
}
//双排:不足 11 个全放九宫格;否则上排固定 5 个,其余在下排自动滚动
Widget _twoAutoScroll(List<AdsInfoModel> list) {
if (list.length < 11) {
return GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 12,
crossAxisSpacing: 10,
childAspectRatio: 60 / 82,
),
itemCount: list.length,
itemBuilder: (_, index) => _item(list[index]),
);
}
final head = list.take(5).toList();
final foot = list.sublist(5);
return LayoutBuilder(
builder: (_, constraints) {
//下排一格和上排九宫格一样宽:4 个列间距共 40 + 左右边距各 16
final itemWidth = ((constraints.maxWidth - 40 - 16 * 2) / 5);
return Column(
children: [
GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 12,
crossAxisSpacing: 10,
childAspectRatio: 60 / 82,
),
itemCount: head.length,
itemBuilder: (_, index) => _item(head[index]),
),
12.sizeBoxH,
SizedBox(
height: itemWidth / (60 / 82), //宽高比和上排一致
child: InfiniteMarquee(
frequency: const Duration(milliseconds: 20),
itemBuilder: (_, index) => Container(
width: itemWidth,
margin: const EdgeInsets.only(right: 7),
child: _item(foot[index % foot.length]),
),
),
),
12.sizeBoxH,
],
);
},
);
}
//单排自动滚动的大图,最多 20 个;一屏 3.2 个,露出下一张引导滑动
Widget _oneScrollBig(List<AdsInfoModel> list) {
return LayoutBuilder(
builder: (_, constraints) {
final data = list.length > 20 ? list.take(20).toList() : list;
final itemWidth = ((constraints.maxWidth - 7 * 2 - 16 * 2) / 3.2);
return Container(
margin: padding,
height: itemWidth / (104 / 176),
child: InfiniteMarquee(
frequency: const Duration(milliseconds: 20),
itemBuilder: (_, index) => Container(
width: itemWidth,
margin: const EdgeInsets.only(right: 7),
child: _item(data[index % data.length], aspectRatio: 104 / 152),
),
),
);
},
);
}
}