Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-11-05 10:38:11 +07:00
co-authored by Claude Opus 5.5
commit 2d26ea8f90
1071 changed files with 100151 additions and 0 deletions
+349
View File
@@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
import 'package:hjsq/hj_utils/screen.dart';
import 'package:hjsq/tools_base/widget/card_swiper/src/swiper.dart';
import 'package:hjsq/tools_base/widget/image_browser_page.dart';
import 'package:hjsq/tools_base/widget/net_image_widget.dart';
import '../../hj_model/splash/ads_model.dart';
import 'ads_item.dart';
/// 广告轮播的指示器位置
enum IndicatorAlign {
bottomRight, //右下角(默认)
bottomCenter, //底部居中,小号主题黄点(视频广告 cell 用)
underCenter, //轮播下方居中(Column 布局,轮播固定高 256)
}
/// 广告轮播:多张时自动轮播 + 指示器
class AdsBanner extends StatefulWidget {
final List<AdsInfoModel>? models; //广告数据
final double? width;
final double? height;
final int autoPlayMs; //自动轮播间隔(毫秒)
final double borderRadius; //图片圆角
final ValueChanged<int>? onTap; //点击某页,传 index
final ValueChanged<int>? onIndexChanged; //切换页,传 index
// ===== 指示器 =====
final IndicatorAlign indicatorAlign;
final IndicatorStyle indicatorStyle;
final Color? color; //未选中色,不传按位置/样式取默认
final Color? selectColor; //选中色,不传按位置/样式取默认
const AdsBanner(
this.models, {
super.key,
this.width,
this.height,
this.autoPlayMs = 5000,
this.borderRadius = 12,
this.onTap,
this.onIndexChanged,
this.indicatorAlign = IndicatorAlign.bottomRight,
this.indicatorStyle = IndicatorStyle.bar,
this.color,
this.selectColor,
});
@override
State<AdsBanner> createState() => _AdsBannerState();
}
class _AdsBannerState extends State<AdsBanner> {
//当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper)
final _index = ValueNotifier(0);
int get _count => widget.models?.length ?? 0;
@override
void dispose() {
_index.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
//指示器放轮播下方:轮播固定高 256(唯一调用方不传 height,靠它撑开 Swiper)
if (widget.indicatorAlign == IndicatorAlign.underCenter) {
return SizedBox(
width: widget.width,
height: widget.height,
child: Column(
children: [
SizedBox(height: 256, child: _swiper()),
12.sizeBoxH,
if (_count > 1)
Center(
child: _indicator(
space: 4,
dotSize: 4,
selectWidth: 8,
color: widget.color,
selectColor: widget.selectColor)),
],
),
);
}
return SizedBox(
width: widget.width,
height: widget.height,
child: Stack(
clipBehavior: Clip.none,
children: [
_swiper(),
//指示器:只有一张时不出
if (_count > 1)
widget.indicatorAlign == IndicatorAlign.bottomCenter
? Positioned(
bottom: 10,
left: 0,
right: 0,
child: Center(
child: _indicator(
space: 2,
dotSize: 2,
selectWidth: 5,
//视频广告 cell:主题黄(未选中半透明,选中实心)
color: widget.color ??
const Color(0xffFFD900).withValues(alpha: 0.4),
selectColor:
widget.selectColor ?? const Color(0xffFFD900),
),
),
)
: Positioned(
bottom: 7,
right: 7,
child: _indicator(
space: 4,
dotSize: 4,
selectWidth: 8,
color: widget.color,
selectColor: widget.selectColor),
),
],
),
);
}
//轮播主体;两种指示器布局共用
Widget _swiper() {
return Swiper(
autoplay: _count > 1,
autoplayDelay: widget.autoPlayMs,
loop: _count > 1,
itemBuilder: (_, index) => AdsItem(
adInfo: widget.models![index],
showType: AdShowType.img,
borderRadius: widget.borderRadius,
),
onTap: widget.onTap,
onIndexChanged: (index) {
_index.value = index;
widget.onIndexChanged?.call(index);
},
itemCount: _count,
);
}
//指示器随当前页局部刷新
Widget _indicator({
required double space,
required double dotSize,
required double selectWidth,
Color? color,
Color? selectColor,
}) {
return ValueListenableBuilder<int>(
valueListenable: _index,
builder: (_, index, __) => DotIndicator(
count: _count,
index: index,
space: space,
dotSize: dotSize,
selectWidth: selectWidth,
style: widget.indicatorStyle,
color: color,
selectColor: selectColor,
),
);
}
}
/// 指示器样式
enum IndicatorStyle {
bar, //选中项拉成长条,默认红色系
dot, //全是圆点,默认白色系
}
/// 分页指示器:选中项宽度与颜色平滑过渡(类似 Android 系统页面指示器)
class DotIndicator extends StatelessWidget {
final int count; //圆点总数
final int index; //当前选中页
final double space; //每个点左侧的间距
final double dotSize; //圆点直径
final double selectWidth; //选中长条宽度,仅 bar 样式生效
final IndicatorStyle style;
final Color? color; //未选中色,不传按样式取默认
final Color? selectColor; //选中色,不传按样式取默认
const DotIndicator({
super.key,
required this.count,
required this.index,
this.space = 5,
this.dotSize = 6,
this.selectWidth = 12,
this.style = IndicatorStyle.bar,
this.color,
this.selectColor,
});
@override
Widget build(BuildContext context) {
final isBar = style == IndicatorStyle.bar;
final activeColor =
selectColor ?? (isBar ? const Color(0xffE1351F) : Colors.white);
final inactiveColor = color ??
(isBar
? const Color(0xffE1351F).withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.5));
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < count; i++)
Padding(
padding: EdgeInsets.only(left: space),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: i == index && isBar ? selectWidth : dotSize,
height: dotSize,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dotSize / 2),
color: i == index ? activeColor : inactiveColor,
),
),
),
],
);
}
}
/// 图片轮播:左右滑动浏览,点图看大图,多张时右下角出指示器
class ImageSwiper extends StatefulWidget {
final List<String>? imgs;
final double aspectRatio;
const ImageSwiper({super.key, this.imgs, required this.aspectRatio});
@override
State<ImageSwiper> createState() => _ImageSwiperState();
}
class _ImageSwiperState extends State<ImageSwiper> {
//当前页用 ValueNotifier 局部刷新指示器,避免每次滑动都 setState 重建 Swiper
final _index = ValueNotifier(0);
int get _count => widget.imgs?.length ?? 0;
@override
void dispose() {
_index.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: widget.aspectRatio,
child: Stack(
alignment: Alignment.center,
children: [
Swiper(
autoplay: _count > 1,
loop: _count > 1,
itemBuilder: (_, index) => InkWell(
enableFeedback: false,
onTap: () =>
ImageBrowserPage.open(widget.imgs ?? [], index: index),
child: NetworkImageLoader(imageUrl: widget.imgs![index]),
),
onIndexChanged: (index) => _index.value = index,
itemCount: _count,
),
if (_count > 1)
Positioned(
bottom: 10,
right: 12,
child: ValueListenableBuilder<int>(
valueListenable: _index,
builder: (_, index, __) =>
DotIndicator(count: _count, index: index, space: 6),
),
),
],
),
);
}
}
/// AI 图生视频 banner:每页左原图、右生成图,多张时自动轮播
class AiBanner extends StatefulWidget {
final List<AdsInfoModel>? models;
const AiBanner({super.key, this.models});
@override
State<AiBanner> createState() => _AiBannerState();
}
class _AiBannerState extends State<AiBanner> {
//当前页用 ValueNotifier 局部刷新指示器,避免滑动重建 Swiper
final _index = ValueNotifier(0);
int get _count => widget.models?.length ?? 0;
@override
void dispose() {
_index.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Swiper(
autoplay: _count > 1,
loop: _count > 1,
autoplayDelay: 5000,
itemBuilder: (_, index) {
final item = widget.models?[index];
//左:原图 右:AI 生成图
return Row(children: [
Expanded(
child: NetworkImageLoader(
imageUrl: item?.cover ?? '', borderRadius: 0)),
Expanded(
child: NetworkImageLoader(
imageUrl: item?.newUrl ?? '', borderRadius: 0)),
]);
},
onIndexChanged: (index) => _index.value = index,
itemCount: _count,
),
if (_count > 1)
Positioned(
bottom: 10,
right: 10,
child: ValueListenableBuilder<int>(
valueListenable: _index,
builder: (_, index, __) =>
DotIndicator(count: _count, index: index),
),
),
],
);
}
}