初始化

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,58 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/history_util.dart';
import 'search_result_page/search_result_main_page.dart';
/// 搜索主页逻辑:只管搜索框回填和搜索历史,热门排行由 RankSubPage 自己取数
class SearchMainLogic extends GetxController {
SearchMainLogic({this.initialResultType});
/// 搜索框控制器:由 logic 持有,点热搜词/历史项后能直接回填输入框
final searchTextCtr = TextEditingController();
/// 搜索历史(展示用):去重、排序、上限都由 SearchHistoryStore 负责,这里只缓存结果
final histories = <String>[];
/// 结果页默认 Tab(短剧频道进入时为 [MediaStyle.Drama]
final MediaStyle? initialResultType;
@override
void onReady() {
super.onReady();
loadHistories();
}
@override
void onClose() {
searchTextCtr.dispose();
super.onClose();
}
/// 重读搜索历史
Future<void> loadHistories() async {
final list = await HistoryUtil.searchHistories();
histories
..clear()
..addAll(list);
update(['history']);
}
/// 清空搜索历史
Future<void> clearHistories() async {
await HistoryUtil.clearSearch();
histories.clear();
update(['history']);
}
/// 提交搜索:记一条历史 → 跳结果页 → 返回后重读(用户此时才重新看到历史列表)
/// 空串由 SearchHistoryStore.save 内部忽略,不入库
Future<void> onSubmitted(String text) async {
searchTextCtr.text = text;
HistoryUtil.addSearch(text); //不 await:落盘不该挡住跳转,返回时早已写完
await Get.to(
() => SearchResultMainPage(text, initialType: initialResultType));
loadHistories();
}
}
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/tools_base/banner/ads_grid_view_widget.dart';
import '../../find/rank_module/rank_main_page.dart';
import '../../find/rank_module/rank_sub_page.dart';
import 'search_main_logic.dart';
import 'widget/search_app_bar.dart';
import 'widget/search_history_view.dart';
import 'widget/search_hot_tag_view.dart';
/// 搜索主页:头部是广告 / 搜索历史 / 热搜词,body 挂视频周榜,两者联动滚动
class SearchMainPage extends StatelessWidget {
/// 进入页面时预填的搜索词(可为空)
final String? searchText;
/// 结果页默认 Tab;短剧频道进入时传 [MediaStyle.Drama]
final MediaStyle? initialResultType;
const SearchMainPage({super.key, this.searchText, this.initialResultType});
@override
Widget build(BuildContext context) {
return GetBuilder<SearchMainLogic>(
init: SearchMainLogic(initialResultType: initialResultType),
builder: (logic) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
children: [
SearchAppBar(
isClose: true,
searchText: searchText,
controller: logic.searchTextCtr, //由 logic 持有,点热搜词/历史项后回填输入框
onSubmitted: logic.onSubmitted,
),
Expanded(
child: NestedScrollView(
headerSliverBuilder: (_, __) => _headerSlivers(logic),
body: const RankSubPage(
pageType: MediaStyle.Video, sortType: 2), // 2 = 周榜
),
),
],
),
);
},
);
}
List<Widget> _headerSlivers(SearchMainLogic logic) {
return [
//顶部广告网格(position=32)
SliverToBoxAdapter(
child: AdsGridViewWidget(
32,
accordingAdsType: true,
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 12.w),
),
),
//搜索历史:随增删局部刷新,不带动整页(id 要和 logic 里 update 的一致)
SliverToBoxAdapter(
child: GetBuilder<SearchMainLogic>(
id: 'history',
builder: (_) => SearchHistoryView(
histories: logic.histories,
onHistoryClick: logic.onSubmitted,
onClearAll: logic.clearHistories,
),
),
),
//热搜词
SliverToBoxAdapter(
child: SearchHotTagView(onTagClick: logic.onSubmitted)),
//「热门排行」标题行,右侧跳完整排行榜页
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 20, 12, 10),
child: Row(
children: [
const Text(
'热门排行',
style: TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 18,
fontWeight: FontWeight.w600),
),
const Spacer(),
InkWell(
enableFeedback: false,
onTap: () => Get.to(() => const RankMainPage(),
preventDuplicates: false),
child: Container(
width: 54,
height: 18,
alignment: Alignment.centerRight,
margin: const EdgeInsets.only(left: 8),
child: Row(
children: [
Text('更多',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 14)),
Icon(Icons.navigate_next,
size: 22,
color: Colors.white.withValues(alpha: 0.55)),
],
),
),
),
],
),
),
),
];
}
}
@@ -0,0 +1,99 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hgdj/hj_utils/api_service/acg_service.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../../hj_model/acg/cartoon_more_list.dart';
import '../../../../hj_model/cartoon_media_info.dart';
import '../../../../tools_base/refresh/pull_refresh.dart';
import '../../../cartoon/acg_widget_item.dart';
class SearchACGResultPage extends StatefulWidget {
final String keywords;
final MediaStyle type;
SearchACGResultPage({
super.key,
required this.type,
required this.keywords,
});
@override
State<SearchACGResultPage> createState() => _SearchACGResultPageState();
}
class _SearchACGResultPageState extends State<SearchACGResultPage> {
RefreshController? refreshController; //由 pullYsRefresh 的 onInit 注入,组件负责释放
int pageNumber = 1;
List<CartoonMediaInfo>? videoList;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_loadData();
});
}
_loadData({int page = 1, int size = 12}) async {
try {
MediaSearchListModel? retResp = await ACGService.mediaSearch(
keyword: widget.keywords,
page: page,
size: size,
kind: widget.type.searchKind);
pageNumber = page;
videoList ??= [];
if (page == 1) {
videoList?.clear();
}
videoList?.addAll(retResp?.list ?? []);
retResp?.hasNext == false
? refreshController?.loadNoData()
: refreshController?.loadComplete();
} catch (e) {
refreshController?.loadComplete();
debugLog(e);
}
refreshController?.refreshCompleted();
videoList ??= [];
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
if (videoList == null) {
return LoadingCenterWidget();
} else if (videoList?.isEmpty == true) {
return CErrorWidget(
retryOnTap: () {
videoList = null;
setState(() {});
_loadData();
},
);
} else {
return pullYsRefresh(
onInit: (ctr) => refreshController = ctr,
onLoading: (ctr) => _loadData(page: pageNumber + 1),
onRefresh: (ctr) => _loadData(),
child: GridView.builder(
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: 111 / 190,
),
itemCount: videoList?.length ?? 0,
itemBuilder: (context, index) {
return AcgItemWidget(info: videoList![index]);
},
),
);
}
}
}
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/history_util.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../../../../tools_base/indicator/custom_tab_indicator.dart';
import '../widget/search_app_bar.dart';
import 'search_acg_result_page.dart';
import 'search_video_result_page.dart';
class SearchResultMainPage extends StatefulWidget {
final String keywords;
/// 从短剧频道进入时默认切到短剧 Tab(对应 kind=4)
final MediaStyle? initialType;
const SearchResultMainPage(this.keywords, {super.key, this.initialType});
@override
State<StatefulWidget> createState() {
return _SearchResultMainPageState();
}
}
class _SearchResultMainPageState extends State<SearchResultMainPage>
with SingleTickerProviderStateMixin {
final typeList = ['影片', '短剧', '抖音', '漫画', '动漫', '帖子', '图集'];
late final TabController tabCtr;
late String keywords = widget.keywords;
static int _tabIndexOf(MediaStyle? type) => switch (type) {
MediaStyle.Drama => 1,
MediaStyle.ShortVideo => 2,
MediaStyle.Comics => 3,
MediaStyle.Cartoon => 4,
MediaStyle.Community => 5,
MediaStyle.Pic => 6,
_ => 0,
};
@override
void initState() {
super.initState();
tabCtr = TabController(
length: typeList.length,
vsync: this,
initialIndex: _tabIndexOf(widget.initialType),
);
}
@override
void dispose() {
tabCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SearchAppBar(
searchText: keywords,
onSubmitted: (value) {
HistoryUtil.addSearch(value); //结果页里再次搜索同样记历史
keywords = value;
setState(() {});
},
),
Container(
height: 32,
margin: EdgeInsets.symmetric(horizontal: 12),
child: TabBar(
tabAlignment: TabAlignment.fill,
tabs: typeList.map((e) {
return Container(
alignment: Alignment.center,
child: Text(e),
);
}).toList(),
labelStyle: TextStyle(fontSize: 14),
labelColor: Color(0xE5FFFFFF),
unselectedLabelStyle: TextStyle(fontSize: 14),
unselectedLabelColor: Color(0x73FFFFFF),
controller: tabCtr,
padding: EdgeInsets.zero,
isScrollable: false,
labelPadding: EdgeInsets.zero,
indicator: const CustomIndicator(
height: 4, width: 16, isGradient: true, offsetY: 4),
),
),
Expanded(
child: TabBarView(
controller: tabCtr,
children: [
SearchVideoResultPage(
key: ValueKey("0$keywords"),
keywords: keywords,
type: MediaStyle.Video)
.keepAlive,
SearchVideoResultPage(
key: ValueKey("1$keywords"),
keywords: keywords,
type: MediaStyle.Drama)
.keepAlive,
SearchVideoResultPage(
key: ValueKey("2$keywords"),
keywords: keywords,
type: MediaStyle.ShortVideo)
.keepAlive,
SearchACGResultPage(
key: ValueKey("3$keywords"),
keywords: keywords,
type: MediaStyle.Comics)
.keepAlive,
SearchACGResultPage(
key: ValueKey("4$keywords"),
keywords: keywords,
type: MediaStyle.Cartoon)
.keepAlive,
SearchVideoResultPage(
key: ValueKey("5$keywords"),
keywords: keywords,
type: MediaStyle.Community)
.keepAlive,
SearchVideoResultPage(
key: ValueKey("6$keywords"),
keywords: keywords,
type: MediaStyle.Pic)
.keepAlive,
],
),
),
],
),
);
}
}
@@ -0,0 +1,421 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/drama_media_info.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/drama/drama_detail_page.dart';
import 'package:hgdj/hj_page/drama/drama_list_page.dart';
import 'package:hgdj/hj_utils/api_service/drama_service.dart';
import 'package:hgdj/hj_utils/api_service/search_service.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../../hj_model/home/video_list_model.dart';
import '../../../../tools_base/refresh/pull_refresh.dart';
import '../../../../tools_base/widget/multitap_recognizer.dart';
import '../../../cartoon/photo_gallery_item.dart';
import '../../../community/widget/community_post_widget.dart';
import '../../home_cell_style/video_simple_cell.dart';
import '../../home_cell_style/divider_tab_bar.dart';
import '../../tag/video_tag_page.dart';
//视频搜索结果排序 tab(标题与接口 sort 值绑定)
const _sortTabs = [
SortTab('最多收藏', 3),
SortTab('最新上架', 2),
SortTab('最多观看', 1),
];
class SearchVideoResultPage extends StatefulWidget {
final String keywords;
final MediaStyle type;
SearchVideoResultPage({
super.key,
required this.type,
required this.keywords,
});
@override
State<SearchVideoResultPage> createState() => _SearchVideoResultPageState();
}
class _SearchVideoResultPageState extends State<SearchVideoResultPage> {
RefreshController? refreshController; //由 pullYsRefresh 的 onInit 注入,组件负责释放
int pageNumber = 1;
List<VideoModel>? videoList;
List<VideoModel>? tagVidList;
String? tagID;
int sortIndex = 0;
bool get _isDrama => widget.type == MediaStyle.Drama;
int get sortParam => _sortTabs[sortIndex].sort;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_loadData(sortParam);
});
}
_loadData(int sortValue, {int page = 1, int size = 12}) async {
try {
if (_isDrama) {
final retResp = await DramaService.search(
keyword: widget.keywords,
page: page,
size: size,
);
pageNumber = page;
if (page == 1) {
videoList = [];
tagVidList =
retResp?.tagMediaList.map((e) => e.toVideoModel(null)).toList() ??
[];
tagID = retResp?.tagID;
}
videoList ??= [];
videoList?.addAll(retResp?.list.map((e) => e.toVideoModel(null)) ?? []);
retResp?.hasNext != true
? refreshController?.loadNoData()
: refreshController?.loadComplete();
} else {
VideoListResp? retResp = await SearchService.searchMedia(
widget.keywords,
pageNumber: page,
pageSize: size,
realm: widget.type.searchRealm,
sortType: sortValue);
pageNumber = page;
videoList ??= [];
videoList?.addAll(retResp?.videos ?? []);
if (page == 1) {
tagVidList = retResp?.tagVidList ?? [];
tagID = retResp?.tagID;
}
retResp?.hasNext == false
? refreshController?.loadNoData()
: refreshController?.loadComplete();
}
} catch (e) {
refreshController?.loadComplete();
debugLog(e);
}
refreshController?.refreshCompleted();
videoList ??= [];
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
if (videoList == null && tagVidList == null) {
return LoadingCenterWidget();
} else if (videoList?.isEmpty == true && tagVidList?.isEmpty == true) {
return CErrorWidget(
retryOnTap: () {
videoList = null;
setState(() {});
_loadData(sortParam);
},
);
} else {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
key: ValueKey(tagID),
child: _buildTagWidget(),
)
];
},
body: Column(
children: [
if (widget.type == MediaStyle.Video ||
widget.type == MediaStyle.ShortVideo)
DividerTabBar(
_sortTabs.map((e) => e.name).toList(),
selectIndex: sortIndex,
alignment: Alignment.center,
callback: (value) {
sortIndex = value;
videoList = null;
_loadData(sortParam);
setState(() {});
},
),
Expanded(
child: _buildContent(),
),
],
),
);
}
}
Widget _buildContent() {
if (videoList == null) {
return LoadingCenterWidget();
} else if (videoList?.isEmpty == true) {
return CErrorWidget(
retryOnTap: () {
videoList = null;
setState(() {});
_loadData(sortParam);
},
);
} else {
return pullYsRefresh(
onInit: (ctr) => refreshController = ctr,
onLoading: (ctr) => _loadData(sortParam, page: pageNumber + 1),
onRefresh: (ctr) => _loadData(sortParam),
child: _buildTypeList(),
);
}
}
/// 进短剧二级页;搜索列表不一定带 dramaInfo,最少用 id/标题/封面兜底
void _openDrama(VideoModel model) {
final drama = model.dramaInfo ??
(DramaMediaInfo()
..id = model.id
..title = model.title
..verticalCover = model.cover
..horizontalCover = model.cover
..totalEpisode = model.totalEpisode);
Get.to(() => DramaDetailPage(drama: drama));
}
Widget _buildTypeList() {
if (widget.type == MediaStyle.Drama) {
return GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: 168 / 266,
),
itemCount: videoList?.length ?? 0,
itemBuilder: (context, index) {
final model = videoList![index];
return MultiTap(
onTap: () => _openDrama(model),
child: VideoSimpleCell(
videoModel: model,
textLines: 1,
coverRightText: model.dramaInfo?.episodeNumberStatus,
showLevelIcon: false,
isFromSearch: true,
onTap: () => _openDrama(model),
),
);
},
);
}
if (widget.type == MediaStyle.Video ||
widget.type == MediaStyle.ShortVideo) {
bool isShort = widget.type == MediaStyle.ShortVideo;
return GridView.builder(
padding: EdgeInsets.only(left: 12, right: 12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isShort ? 3 : 2,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: isShort ? 111 / 190 : 168 / 154,
),
itemCount: videoList?.length ?? 0,
itemBuilder: (context, index) {
return VideoSimpleCell(
videoModel: videoList![index],
textLines: isShort ? 1 : 2,
isFromSearch: true,
);
},
);
} else if (widget.type == MediaStyle.Pic) {
return GridView.builder(
padding: EdgeInsets.only(left: 12, right: 12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: 111 / 190,
),
itemCount: videoList?.length ?? 0,
itemBuilder: (context, index) {
return PhotoGalleryItem(videoModel: videoList![index], textline: 1);
},
);
} else {
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: videoList?.length ?? 0,
itemBuilder: (context, index) {
return CommunityPostWidget(
videoModel: videoList![index],
videoModels: videoList,
);
},
);
}
}
Widget _buildTagWidget() {
if (tagVidList?.isNotEmpty != true) return SizedBox();
if (_isDrama) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.keywords} 标签内容',
style: const TextStyle(color: Color(0xE5FFFFFF), fontSize: 14),
),
12.sizeBoxH,
GridView.builder(
itemCount: min(4, tagVidList?.length ?? 0),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: 168 / 266,
),
itemBuilder: (context, index) {
final videoModel = tagVidList![index];
return MultiTap(
onTap: () => _openDrama(videoModel),
child: VideoSimpleCell(
videoModel: videoModel,
textLines: 1,
coverRightText: videoModel.dramaInfo?.episodeNumberStatus,
showLevelIcon: false,
isFromSearch: true,
onTap: () => _openDrama(videoModel),
),
);
},
),
if (tagID?.isNotEmpty == true) ...[
const SizedBox(height: 12),
GestureDetector(
onTap: () => DramaListPage.toTag(
TagsBean(id: tagID, name: widget.keywords)),
child: Container(
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x1AFFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('查看更多',
style: TextStyle(
color: Color(0xffEFEFEF), fontSize: 14)),
SizedBox(width: 6),
Icon(Icons.keyboard_arrow_right,
color: Color(0xffDCDCDC), size: 18),
],
),
),
),
],
const SizedBox(height: 6),
],
),
);
}
if (widget.type == MediaStyle.Video ||
widget.type == MediaStyle.ShortVideo) {
bool isShort = widget.type == MediaStyle.ShortVideo;
return Container(
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${widget.keywords} 标签内容",
style: TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 14,
),
),
12.sizeBoxH,
GridView.builder(
itemCount: min(isShort ? 6 : 4, tagVidList?.length ?? 0),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isShort ? 3 : 2,
mainAxisSpacing: 12,
crossAxisSpacing: 6,
childAspectRatio: isShort ? 111 / 190 : 168 / 154,
),
itemBuilder: (context, index) {
VideoModel videoModel = tagVidList![index];
return VideoSimpleCell(
videoModel: videoModel,
textLines: isShort ? 1 : 2,
isFromSearch: true,
);
},
),
SizedBox(height: 12),
GestureDetector(
onTap: () {
Get.to(
VideoTagPage(
TagsBean(id: tagID, name: widget.keywords),
isFromSearch: true,
isShortStyle: widget.type == MediaStyle.ShortVideo,
),
preventDuplicates: false);
},
child: Container(
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Color(0x1AFFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"查看更多",
style: TextStyle(
color: Color(0xffEFEFEF),
fontSize: 14,
),
),
SizedBox(width: 6),
Icon(
Icons.keyboard_arrow_right,
color: Color(0xffDCDCDC),
size: 18,
),
],
),
),
),
SizedBox(height: 6),
],
),
);
}
return SizedBox();
}
}
@@ -0,0 +1,147 @@
import 'package:flutter/material.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/video_library_model.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../hj_model/video_model.dart';
import '../../../hj_utils/api_service/vid_service.dart';
import '../../../tools_base/debug_log.dart';
import '../../../tools_base/loading/loading_alert_widget.dart';
class VideoAllTypeLogic extends GetxController with GetSingleTickerProviderStateMixin {
VideoAllTypeLogic();
HomeVideoLibrary? homeVideoLibrary;
TimeType? firstSelectedType;
TimeType? secondSelectedType;
Tags? thirdSelectedType;
TimeType? forthSelectedType;
TimeType? fivthSelectedType;
RefreshController? refreshController;
List<VideoModel>? searchVideoList;
List<CartoonMediaInfo>? searchACGList;
Keyword? preKeyword;
int currentPage = 1;
RxBool isInitData = true.obs;
RxBool showFloatingTags = false.obs;
List<Tags>? get tags {
if (firstSelectedType?.isACG == true) {
return homeVideoLibrary?.acgTags;
} else {
return homeVideoLibrary?.vidTags;
}
}
String get selectedTagsText {
// 上滑吸顶摘要:展示分类/排序/标签/时间四个维度的当前选中值;paymentType 无入口不参与。
// 「全部」为默认兜底选项,摘要里省略避免多出一段无意义文案。
final names = [
firstSelectedType?.name,
secondSelectedType?.name,
thirdSelectedType?.name,
fivthSelectedType?.name,
];
return names.whereType<String>().where((e) => e.isNotEmpty && e != '全部').join('·');
}
Keyword get keywordValue {
Keyword keyword = Keyword();
keyword.tags = thirdSelectedType;
keyword.canvas = firstSelectedType;
keyword.orderBy = secondSelectedType;
keyword.paymentType = forthSelectedType;
keyword.timeType = fivthSelectedType;
return keyword;
}
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_loadMenuData();
});
}
void reloadMenuData() {
isInitData.value = true;
_loadMenuData();
}
void _loadMenuData() async {
try {
homeVideoLibrary = await VidService.fetchLibrary();
firstSelectedType = homeVideoLibrary?.canvas?.first;
secondSelectedType = homeVideoLibrary?.orderBy?.first;
thirdSelectedType = homeVideoLibrary?.vidTags?.first;
forthSelectedType = homeVideoLibrary?.paymentType?.first;
fivthSelectedType = homeVideoLibrary?.timeType?.first;
_loadSearchData();
} catch (e) {
debugLog(e);
}
homeVideoLibrary ??= HomeVideoLibrary();
isInitData.value = false;
update();
}
void reloadData() {
searchVideoList = null;
update();
_loadSearchData();
}
void refreshData() {
_loadSearchData();
}
void loadMoreData() {
_loadSearchData(page: currentPage + 1);
}
void menuExchangeEvent() {
update();
_loadSearchData(isSelectMenu: true);
}
void _loadSearchData({int page = 1, int size = 10, bool isSelectMenu = false}) async {
try {
if (isSelectMenu) {
LoadingAlertWidget.show();
}
Keyword keywordParam = keywordValue;
HomeVideoLibraryResult? respResult = await VidService.searchLibrary(
page,
size,
filterMenu: keywordParam,
);
if (keywordValue.modelKey != keywordParam.modelKey) {
return;
}
if (isSelectMenu) {
LoadingAlertWidget.cancel();
}
searchVideoList ??= [];
searchACGList ??= [];
currentPage = page;
if (page == 1) {
searchVideoList?.clear();
searchACGList?.clear();
}
searchVideoList?.addAll(respResult?.list ?? []);
searchACGList?.addAll(respResult?.allMediaList ?? []);
respResult?.hasNext == true ? refreshController?.loadComplete() : refreshController?.loadNoData();
} catch (e) {
if (isSelectMenu) {
LoadingAlertWidget.cancel();
}
debugLog(e);
refreshController?.loadComplete();
}
refreshController?.refreshCompleted();
searchVideoList ??= [];
update();
}
}
@@ -0,0 +1,387 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_model/cartoon_media_info.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
import '../../../hj_model/home/video_library_model.dart';
import '../../../hj_model/home/module_detail_model.dart';
import '../../../hj_model/video_model.dart';
import '../../../tools_base/loading/loading_center_widget.dart';
import '../../../tools_base/refresh/pull_refresh.dart';
import '../../cartoon/acg_widget_item.dart';
import '../../cartoon/photo_gallery_item.dart';
import '../../community/widget/community_post_widget.dart';
import '../home_cell_style/video_simple_cell.dart';
import 'video_all_type_logic.dart';
//片库
class VideoAllTypePage extends StatefulWidget {
const VideoAllTypePage({super.key});
@override
State<VideoAllTypePage> createState() => _VideoAllTypePageState();
}
class _VideoAllTypePageState extends State<VideoAllTypePage> {
final ScrollController scrollController = ScrollController();
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GetBuilder<VideoAllTypeLogic>(
init: VideoAllTypeLogic(),
builder: (logic) {
// 摘要文案随筛选变化,靠 GetBuilder 的 update() 重算;下方 Obx 只管吸顶显隐
final tagsText = logic.selectedTagsText;
return Scaffold(
appBar: AppBar(title: const Text('片库')),
body: Stack(
children: [
Obx(
() {
if (logic.isInitData.value) {
return LoadingCenterWidget();
} else if (logic.homeVideoLibrary?.isNotEmpty != true) {
return CErrorWidget(
retryOnTap: () {
logic.reloadMenuData();
},
);
} else {
return NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo.metrics.pixels > 200) {
logic.showFloatingTags.value = true;
} else {
logic.showFloatingTags.value = false;
}
return true;
},
child: pullYsRefresh(
onInit: (ctr) => logic.refreshController = ctr,
onLoading: (_) => logic.loadMoreData(),
onRefresh: (_) => logic.refreshData(),
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
_buildItemMenuWidget(
"分类",
logic.homeVideoLibrary?.canvas,
logic.firstSelectedType, (data) {
if (logic.firstSelectedType?.isACG !=
data.isACG) {
logic.thirdSelectedType = null;
}
if (logic.firstSelectedType != data) {
logic.firstSelectedType = data;
} else {
logic.firstSelectedType = null;
}
logic.searchVideoList?.clear();
logic.menuExchangeEvent();
}),
_buildItemMenuWidget(
"排序",
logic.homeVideoLibrary?.orderBy,
logic.secondSelectedType, (data) {
if (logic.secondSelectedType != data) {
logic.secondSelectedType = data;
} else {
logic.secondSelectedType = null;
}
logic.menuExchangeEvent();
}),
_buildTagMenuWidget(
"标签", logic.tags, logic.thirdSelectedType,
(data) {
if (logic.thirdSelectedType != data) {
logic.thirdSelectedType = data;
} else {
logic.thirdSelectedType = null;
}
logic.menuExchangeEvent();
}),
_buildItemMenuWidget(
"时间",
logic.homeVideoLibrary?.timeType,
logic.fivthSelectedType, (data) {
if (logic.fivthSelectedType != data) {
logic.fivthSelectedType = data;
} else {
logic.fivthSelectedType = null;
}
logic.menuExchangeEvent();
}),
],
),
),
_buildTableContent(logic),
],
),
),
);
}
},
),
Obx(() {
if (!logic.showFloatingTags.value) return const SizedBox();
if (tagsText.isEmpty) return const SizedBox();
return Positioned(
top: 0,
left: 0,
right: 0,
child: GestureDetector(
onTap: () {
// 滚动到页面顶部
if (scrollController.hasClients) {
scrollController.animateTo(
0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
tagsText,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}),
],
),
);
},
);
}
Widget _buildConditionItem(String? title, bool isSelected) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 6),
margin: EdgeInsets.only(right: 5),
alignment: Alignment.centerLeft,
child: Text(
title ?? "",
style: TextStyle(
color: isSelected ? AppColors.actionRed : Color(0x8CFFFFFF),
fontSize: 14,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
),
),
);
}
Widget _buildTitleItem(String? typeName) {
return Text(
typeName ?? "",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
);
}
Widget _buildTableContent(VideoAllTypeLogic logic) {
if (logic.searchVideoList == null && logic.searchACGList == null) {
return SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: LoadingCenterWidget(),
),
);
} else if (logic.searchVideoList?.isNotEmpty != true &&
logic.searchACGList?.isNotEmpty != true) {
return SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: CErrorWidget(
retryOnTap: () {
logic.reloadData();
},
),
),
);
} else {
String typeName = logic.firstSelectedType?.name ?? "";
if (typeName == "帖子") {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return CommunityPostWidget(
videoModel: logic.searchVideoList![index],
videoModels: logic.searchVideoList,
);
},
childCount: logic.searchVideoList?.length ?? 0,
),
);
}
if (typeName == "图集") {
return _buildWaterfall(
logic, (vm) => PhotoGalleryItem(videoModel: vm, textline: 1));
}
if (typeName == "抖音" || logic.firstSelectedType?.key == "sp") {
return _buildWaterfall(
logic, (vm) => VideoSimpleCell(videoModel: vm, textLines: 1));
} else if (typeName == "动漫" || typeName == "漫画") {
return SliverPadding(
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12.0,
crossAxisSpacing: 6,
childAspectRatio: 111 / 190,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
CartoonMediaInfo videoItem = logic.searchACGList![index];
return AcgItemWidget(info: videoItem);
},
childCount: logic.searchACGList?.length ?? 0,
),
),
);
} else {
return SliverPadding(
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12.0,
crossAxisSpacing: 6,
childAspectRatio: 168 / 154,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
VideoModel videoItem = logic.searchVideoList![index];
return VideoSimpleCell(
videoModel: videoItem,
);
},
childCount: logic.searchVideoList?.length ?? 0,
),
),
);
}
}
}
//图集/抖音共用的 3 列瀑布流(仅 item 构建不同)
Widget _buildWaterfall(
VideoAllTypeLogic logic, Widget Function(VideoModel vm) itemBuilder) {
return SliverPadding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
sliver: SliverWaterfallFlow(
gridDelegate: const SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 6,
mainAxisSpacing: 12,
),
delegate: SliverChildBuilderDelegate(
(c, index) => AspectRatio(
aspectRatio: 111 / 190,
child: itemBuilder(logic.searchVideoList![index]),
),
childCount: logic.searchVideoList?.length ?? 0,
),
),
);
}
Widget _buildItemMenuWidget(String typeName, List<TimeType>? listData,
TimeType? selectType, Function(TimeType timeType) callBack) {
selectType ??= listData?[0];
return Container(
height: 40,
padding: EdgeInsets.only(left: 16),
child: Row(
children: [
_buildTitleItem(typeName),
SizedBox(width: 12),
Expanded(
child: ListView.builder(
itemCount: listData?.length ?? 0,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(vertical: 8),
itemBuilder: (context, index) {
TimeType data = listData![index];
bool isSelected = selectType?.name == data.name;
return InkWell(
enableFeedback: false,
onTap: () async {
callBack.call(data);
},
child: _buildConditionItem(data.name, isSelected),
);
},
),
),
],
),
);
}
Widget _buildTagMenuWidget(String typeName, List<Tags>? listData,
Tags? selectType, Function(Tags timeType) callBack) {
selectType ??= listData?[0];
return Container(
height: 40,
padding: EdgeInsets.only(left: 16),
child: Row(
children: [
_buildTitleItem(typeName),
SizedBox(width: 12),
Expanded(
child: ListView.builder(
itemCount: listData?.length ?? 0,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(vertical: 8),
itemBuilder: (context, index) {
Tags data = listData![index];
bool isSelected = selectType?.name == data.name;
return GestureDetector(
onTap: () async {
callBack.call(data);
},
child: _buildConditionItem(data.name, isSelected),
);
},
),
),
],
),
);
}
}
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../../tools_base/widget/marquee_widget.dart';
import '../../../mine/welfare/sign_daily_page.dart';
import '../../home_main_logic.dart';
import '../search_main_page.dart';
class CommonSearchBarView extends StatefulWidget {
final HomeMainLogic? logic;
const CommonSearchBarView({super.key, this.logic});
@override
State<CommonSearchBarView> createState() => _CommonSearchBarViewState();
}
class _CommonSearchBarViewState extends State<CommonSearchBarView> {
/// 当前轮播展示的搜索热词,点击搜索框时带入搜索页
String? _searchText;
static final _hintStyle =
TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14);
@override
void initState() {
super.initState();
if (Config.searchHints.isNotEmpty) _searchText = Config.searchHints.first;
}
@override
Widget build(BuildContext context) {
return Row(
children: [
16.w.sizeBoxW,
Image.asset('place_holder_logo.webp'.commonImgPath, height: 28),
8.sizeBoxW,
Expanded(child: _buildSearchBox()),
12.sizeBoxW,
if (Config.signIcon != null && Config.signIcon!.isNotEmpty)
_buildSignIcon(),
12.sizeBoxW,
_buildMenuIcon(),
16.sizeBoxW,
],
);
}
/// 中间搜索框(热词轮播)
Widget _buildSearchBox() {
return InkWell(
enableFeedback: false,
onTap: () => Get.to(SearchMainPage(searchText: _searchText)),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xff1E1C1D),
borderRadius: BorderRadius.circular(99),
),
child: Config.searchHints.isEmpty
? Text('请输入关键字', style: _hintStyle)
: MarqueeWidget(
count: Config.searchHints.length,
onIndexChanged: (index) =>
_searchText = Config.searchHints[index],
itemBuilder: (_, index) => Container(
alignment: Alignment.centerLeft,
child: Text(Config.searchHints[index], style: _hintStyle),
),
),
),
);
}
/// 签到入口
Widget _buildSignIcon() {
return InkWell(
enableFeedback: false,
onTap: () => Get.to(SignDailyPage()),
child: NetworkImageLoader(
imageUrl: Config.signIcon,
width: 20,
borderRadius: 0,
placeHolderWidget: const SizedBox(),
),
);
}
/// 右侧菜单入口
Widget _buildMenuIcon() {
return InkWell(
enableFeedback: false,
onTap: () => widget.logic?.openEndDrawer(),
child: Image.asset('acg_menu.png'.acgImgPath,
width: 24, color: const Color(0xff989898)),
);
}
}
@@ -0,0 +1,189 @@
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/tools_base/toast.dart';
import '../video_all_type_page.dart';
final class SearchAppBar extends StatefulWidget {
final Function(String keywork) onSubmitted;
final bool? isClose;
final String? searchText;
/// 外部持有的输入控制器:搜索主页由 logic 持有,点热搜词/历史项后好回填。
/// 不传则本组件自建自销——每个 SearchAppBar 各用各的,不再共享全局单例
final TextEditingController? controller;
const SearchAppBar({
super.key,
required this.onSubmitted,
this.searchText,
this.isClose,
this.controller,
});
@override
State<SearchAppBar> createState() => _SearchAppBarState();
}
class _SearchAppBarState extends State<SearchAppBar> {
late final searchTextCtr = widget.controller ?? TextEditingController();
late bool showDeleteIcon;
@override
void initState() {
super.initState();
final initText = widget.searchText ?? "";
showDeleteIcon = initText.isNotEmpty;
// 控制器不再跨路由共享,此处赋值不会 markNeedsBuild 别的路由,无需延到首帧后
searchTextCtr.text = initText;
}
@override
void dispose() {
// 只释放自己 new 的;外部传进来的归调用方管
if (widget.controller == null) searchTextCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: screen.paddingTop),
color: Colors.black,
child: Container(
height: 56,
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
GestureDetector(
onTap: () {
setState(() {
showDeleteIcon = false;
searchTextCtr.text = "";
});
Get.back();
},
child: Image.asset(
'common_back.png'.commonImgPath,
width: 22,
),
),
10.sizeBoxW,
Expanded(
child: Container(
height: 32,
padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Color(0x11FFFFFF),
borderRadius: BorderRadius.circular(20)),
child: Row(
children: [
InkWell(
enableFeedback: false,
onTap: () {
Get.to(() => VideoAllTypePage(), opaque: false);
},
child: Row(
children: [
2.sizeBoxW,
Image.asset('libary_search.webp'.homePath, width: 20),
6.sizeBoxW,
Text(
'片库',
style: TextStyle(
color: Color(0xFFF68804),
fontSize: 14,
),
),
6.sizeBoxW,
Container(
height: 12,
width: 1,
color: Colors.white.withValues(alpha: .05),
),
6.sizeBoxW,
],
),
),
Expanded(
child: TextField(
controller: searchTextCtr,
style: TextStyle(
color: Colors.white.withValues(alpha: .8),
fontSize: 14,
height: 1),
onSubmitted: (value) => widget.onSubmitted(value),
onChanged: (value) {
if (value.isNotEmpty == true) {
setState(() {
showDeleteIcon = true;
});
} else {
setState(() {
showDeleteIcon = false;
});
}
},
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: '搜索关键词',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: .55),
fontSize: 14,
height: 1),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
12.sizeBoxW,
Visibility(
visible: showDeleteIcon,
child: GestureDetector(
onTap: () {
setState(() {
searchTextCtr.text = "";
showDeleteIcon = false;
});
},
child: Image.asset('search_close.webp'.homePath,
width: 18),
),
)
],
),
),
),
10.sizeBoxW,
InkWell(
enableFeedback: false,
onTap: () {
if (widget.isClose == true) {
setState(() {
showDeleteIcon = false;
});
}
if (searchTextCtr.text.isEmpty == true) {
showToast("请输入关键字");
return;
}
widget.onSubmitted(searchTextCtr.text);
},
child: Text(
"搜索",
style: TextStyle(
color: Color(0x73FFFFFF),
fontSize: 16,
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,137 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
/// 搜索历史区:纯展示,数据与增删由 SearchMainLogic 持有
class SearchHistoryView extends StatefulWidget {
const SearchHistoryView({
super.key,
required this.histories,
required this.onHistoryClick,
required this.onClearAll,
});
final List<String> histories;
final Function(String keyword) onHistoryClick;
final VoidCallback onClearAll;
@override
State<SearchHistoryView> createState() => _SearchHistoryViewState();
}
class _SearchHistoryViewState extends State<SearchHistoryView> {
/// 折叠状态最多展示几条
static const _collapsedMax = 8;
bool isExpandData = false;
int get itemCount {
final total = widget.histories.length;
return isExpandData ? total : min(_collapsedMax, total);
}
@override
Widget build(BuildContext context) {
if (widget.histories.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'历史记录',
style: TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
InkWell(
enableFeedback: false,
onTap: widget.onClearAll,
child: Container(
padding: const EdgeInsets.all(1),
child: Image.asset("history_delete.webp".homePath,
width: 18, height: 18),
),
),
],
),
12.sizeBoxH,
GridView.builder(
padding: EdgeInsets.zero,
itemCount: itemCount,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 79 / 34,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemBuilder: (_, index) =>
_buildHistoryItem(widget.histories[index]),
),
_buildMoreRecord(),
],
),
);
}
Widget _buildHistoryItem(String tag) {
return GestureDetector(
onTap: () => widget.onHistoryClick(tag),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x0DFFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: Text(
tag,
maxLines: 1,
style: const TextStyle(color: Color(0xE5FFFFFF), fontSize: 12),
),
),
);
}
/// 「查看 / 收起完整记录」按钮:少于 [_collapsedMax] 条时不显示
Widget _buildMoreRecord() {
if (widget.histories.length < _collapsedMax) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.only(top: 12),
alignment: Alignment.topCenter,
child: InkWell(
enableFeedback: false,
onTap: () => setState(() => isExpandData = !isExpandData),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isExpandData ? "收起完整记录" : "查看完整记录",
style: const TextStyle(color: Color(0xff989898), fontSize: 12),
),
4.sizeBoxW,
Transform.rotate(
angle: isExpandData ? pi : 0,
child: Image.asset(
"arrow_down.png".commonImgPath,
width: 12,
color: const Color(0xffDCDCDC),
),
),
],
),
),
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/screen.dart';
class SearchHotTagView extends StatelessWidget {
final Function(String keyword) onTagClick;
const SearchHotTagView({super.key, required this.onTagClick});
@override
Widget build(BuildContext context) {
final tags = Config.hotWords;
if (tags.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.only(left: 12, right: 12, top: 18),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'大家都在搜',
style: TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
12.h.sizeBoxH,
GridView.builder(
padding: EdgeInsets.zero,
itemCount: tags.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 79 / 34,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemBuilder: (_, index) => _buildTagItem(tags[index]),
),
],
),
);
}
Widget _buildTagItem(String tag) {
return GestureDetector(
onTap: () => onTagClick(tag),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x0DFFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: Text(
tag,
maxLines: 1,
style: const TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 12,
),
),
),
);
}
}
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../community/community_tag_page/community_tag_page.dart';
class TopicItem extends StatelessWidget {
final TagsBean model;
final bool isSelect;
final Function()? onTap;
const TopicItem(
{super.key, required this.model, this.onTap, this.isSelect = false});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (onTap != null) {
onTap!();
} else {
Get.to(() => CommunityTagDetailPage(model: model));
}
},
child: Container(
height: 66,
padding: EdgeInsets.only(left: 12.w, right: 12.w),
decoration: BoxDecoration(
color: Color(0x0DFFFFFF),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
NetworkImageLoader(
imageUrl: model.coverImg ?? '',
width: 38,
height: 38,
borderRadius: 3,
),
10.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'#${model.name}',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// 4.sizeBoxH,
Text(
'${model.vidCount?.countStr}个帖子 ${model.playCount.countStr}浏览 ${model.followCount.countStr}关注',
style: TextStyle(color: Color(0x73FFFFFF), fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
12.sizeBoxW,
Container(
width: 52,
height: 26,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelect ? AppColors.actionRed : Color(0x33FFFFFF),
borderRadius: BorderRadius.circular(30),
),
child: Text(
isSelect ? '已选' : "选择",
style: TextStyle(
color: isSelect ? Color(0xffffffff) : Color(0xffDCDCDC),
fontSize: 14,
),
),
),
],
),
),
);
}
}