初始化

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,364 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/card_swiper/src/swiper.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../hj_model/splash/ads_model.dart';
import 'ads_item.dart';
/// 广告轮播
class AdsBannerWidget extends StatefulWidget {
final List<AdsInfoModel>? models; // 广告数据列表
final double? width; // 整体宽度
final double? height; // 整体高度
final ValueChanged<int>? onItemClick; // 点击某页回调(传 index
final ValueChanged<int>? onIndexChanged; // 切换页回调(传 index
final int? autoPlayMs; // 自动轮播间隔(毫秒),默认 5000
final bool isIndicatorBottomCenter; // 指示器置于底部居中
final bool isIndicatorUnderCenter; // 指示器置于轮播下方居中(Column 布局)
final Color? color; // 指示器未选中色
final Color? selectColor; // 指示器选中色
final bool isCircle; // 透传给 CIndicator.isBarStyletrue=长条红色风格)
final double borderRadius; // 图片圆角,默认 12
AdsBannerWidget(
this.models, {
super.key,
this.width,
this.height,
this.autoPlayMs = 5000,
this.onItemClick,
this.onIndexChanged,
this.isIndicatorBottomCenter = false,
this.isIndicatorUnderCenter = false,
this.color,
this.selectColor,
this.isCircle = true,
this.borderRadius = 12,
});
@override
State<AdsBannerWidget> createState() => _AdsBannerWidgetState();
}
class _AdsBannerWidgetState extends State<AdsBannerWidget> {
// 当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper)
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
int get _count => widget.models?.length ?? 0;
@override
void dispose() {
_selectIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.width,
height: widget.height,
child: widget.isIndicatorUnderCenter
? Column(
children: [
// 该分支高度固定 256(唯一调用方不传 height,靠此撑开 Swiper
SizedBox(height: 256, child: _buildSwiper()),
12.sizeBoxH,
if (_count > 1)
Container(
alignment: Alignment.center,
child:
_buildIndicator(space: 4, dotSize: 4, selectWidth: 8),
),
],
)
: Stack(
clipBehavior: Clip.none,
children: [
_buildSwiper(),
if (widget.isIndicatorBottomCenter)
Positioned(
bottom: 10,
left: 0,
right: 0,
child: _count > 1
? Container(
alignment: Alignment.center,
child: _buildIndicator(
space: 2,
dotSize: 2,
selectWidth: 5,
// 视频广告 cell:主题黄(未选中半透明,选中实心)
color: widget.color ??
const Color(0xffFFD900)
.withValues(alpha: 0.4),
selectColor:
widget.selectColor ?? const Color(0xffFFD900),
),
)
: const SizedBox.shrink(),
)
else
Positioned(
bottom: 7,
right: 7,
child: _count > 1
? _buildIndicator(space: 4, dotSize: 4, selectWidth: 8)
: const SizedBox.shrink(),
),
],
),
);
}
/// 轮播主体;两种指示器布局共用
Widget _buildSwiper() {
return Swiper(
autoplay: _count > 1,
autoplayDelay: widget.autoPlayMs ?? 5000,
loop: _count > 1,
itemBuilder: (context, index) => AdsItem(
adInfo: widget.models![index],
showType: AdShowType.img,
borderRadius: widget.borderRadius,
),
onTap: widget.onItemClick,
onIndexChanged: (index) {
_selectIndex.value = index;
widget.onIndexChanged?.call(index);
},
itemCount: _count,
);
}
/// 指示器随当前页局部刷新
Widget _buildIndicator({
required double space,
required double dotSize,
required double selectWidth,
Color? color,
Color? selectColor,
}) {
return ValueListenableBuilder<int>(
valueListenable: _selectIndex,
builder: (_, index, __) => CIndicator(
itemCount: _count,
selectIndex: index,
space: space,
dotSize: dotSize,
selectWidth: selectWidth,
color: color ?? widget.color,
selectColor: selectColor ?? widget.selectColor,
isBarStyle: widget.isCircle,
),
);
}
}
/// 分页指示器
class CIndicator extends StatelessWidget {
const CIndicator({
super.key,
this.itemCount,
this.selectIndex,
this.space = 5.0,
this.isBarStyle = true,
this.color,
this.selectColor,
this.dotSize = 6,
this.selectWidth = 12,
});
final int? itemCount; // 圆点总数
final int? selectIndex; // 当前选中页
final double? space; // 圆点间距
final bool isBarStyle; // true=选中态长条+红色系;false=圆点+白色系
final Color? color; // 未选中色(覆盖默认)
final Color? selectColor; // 选中色(覆盖默认)
final double dotSize; // 圆点直径
final double selectWidth; // 选中态(长条)宽度,isBarStyle 时生效
Widget _buildDot(int index) {
final isSelected = selectIndex == index;
final activeColor =
selectColor ?? (isBarStyle ? const Color(0xffE1351F) : Colors.white);
final inactiveColor = color ??
(isBarStyle
? const Color(0xffE1351F).withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.5));
// 选中态长条、未选中态圆点;宽度与颜色平滑过渡(类似 Android 系统页面指示器)
return Padding(
padding: EdgeInsets.only(left: space ?? 0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: isSelected && isBarStyle ? selectWidth : dotSize,
height: dotSize,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dotSize / 2),
color: isSelected ? activeColor : inactiveColor,
),
),
);
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [for (int i = 0; i < (itemCount ?? 0); i++) _buildDot(i)],
);
}
}
/// 图集:左右滑动浏览,可选右上角页码 + 底部圆点指示器
class ImageCollectionWidget extends StatefulWidget {
final List<String>? imgs;
final double aspectRatio;
final bool showCIndicator; // 是否显示底部圆点指示器
final bool showIndex; // 是否显示右上角页码
ImageCollectionWidget({
super.key,
this.imgs,
this.aspectRatio = 375 / 467,
this.showCIndicator = true,
this.showIndex = true,
});
@override
State<ImageCollectionWidget> createState() => _ImageCollectionWidgetState();
}
class _ImageCollectionWidgetState extends State<ImageCollectionWidget> {
// 当前页用 ValueNotifier 局部刷新页码/指示器,避免每次滑动都 setState 重建 Swiper
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
int get _count => widget.imgs?.length ?? 0;
@override
void dispose() {
_selectIndex.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: (context, index) {
final cover = widget.imgs![index];
return InkWell(
enableFeedback: false,
onTap: () =>
ImageBrowserPage.open(widget.imgs ?? [], index: index),
child: NetworkImageLoader(imageUrl: cover),
);
},
onIndexChanged: (index) => _selectIndex.value = index,
itemCount: _count,
),
// 右上角页码 1/N
if (widget.showIndex && _count > 1)
Positioned(
top: 24,
right: 16,
child: ValueListenableBuilder<int>(
valueListenable: _selectIndex,
builder: (_, index, __) => Container(
padding: const EdgeInsets.symmetric(horizontal: 14),
alignment: Alignment.center,
height: 28,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
color: Colors.black.withValues(alpha: .6),
),
child: Text('${index + 1}/$_count',
style:
const TextStyle(fontSize: 14, color: Colors.white)),
),
),
),
// 底部圆点指示器
if (widget.showCIndicator && _count > 1)
Positioned(
bottom: 10,
right: 12,
child: ValueListenableBuilder<int>(
valueListenable: _selectIndex,
builder: (_, index, __) =>
CIndicator(itemCount: _count, selectIndex: index, space: 6),
),
),
],
),
);
}
}
/// AI 图生视频 banner:每页左原图、右生成图,自动轮播
class AIBannerWidget extends StatefulWidget {
final int index; // 初始选中页
final List<AdsInfoModel>? models;
const AIBannerWidget({super.key, this.index = 0, this.models});
@override
State<AIBannerWidget> createState() => _AIBannerWidgetState();
}
class _AIBannerWidgetState extends State<AIBannerWidget> {
// 当前页用 ValueNotifier 局部刷新指示器,避免滑动重建 Swiper
late final ValueNotifier<int> _selectIndex = ValueNotifier(widget.index);
int get _count => widget.models?.length ?? 0;
@override
void dispose() {
_selectIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(children: [
Swiper(
autoplay: _count > 1,
loop: _count > 1,
autoplayDelay: 5000,
itemBuilder: (context, 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) => _selectIndex.value = index,
itemCount: _count,
),
Positioned(
bottom: 10,
right: 10,
child: _count > 1
? ValueListenableBuilder<int>(
valueListenable: _selectIndex,
builder: (_, index, __) =>
CIndicator(itemCount: _count, selectIndex: index),
)
: const SizedBox.shrink(),
),
]);
}
}