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
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hjsq/assets_tool/images.dart';
import 'package:hjsq/config/config.dart';
import 'package:hjsq/hj_utils/screen.dart';
import '../../../../tools_base/widget/marquee_widget.dart';
import '../../../../tools_base/widget/net_image_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 const _titleStyle = TextStyle(
color: Color(0xFFF2F2F4),
fontSize: 15,
fontWeight: FontWeight.w600,
height: 18 / 15,
letterSpacing: -0.15,
);
static final _hintStyle = TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 13,
height: 18 / 13,
fontWeight: FontWeight.w400,
);
@override
void initState() {
super.initState();
if (Config.searchHints.isNotEmpty) _searchText = Config.searchHints.first;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildLogo(),
10.sizeBoxW,
Expanded(child: _buildSearchBox()),
10.sizeBoxW,
_buildSignIcon(),
],
),
);
}
Widget _buildLogo() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(34),
child: Image.asset('nav_logo.webp'.homePath, width: 28, height: 28),
),
6.sizeBoxW,
const Text('海角社区', style: _titleStyle),
],
);
}
/// 中间搜索框(热词轮播)
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: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withValues(alpha: 0.16)),
),
child: Row(
children: [
Image.asset('search_icon.png'.commonImgPath,
width: 18, height: 18, color: Colors.white),
8.sizeBoxW,
Expanded(
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,
maxLines: 1,
overflow: TextOverflow.ellipsis),
),
),
),
],
),
),
);
}
/// 签到入口
Widget _buildSignIcon() {
return InkWell(
enableFeedback: false,
onTap: () => Get.to(SignDailyPage()),
child: Container(
width: 32,
height: 32,
alignment: Alignment.center,
child: Config.signIcon?.isNotEmpty == true
? NetworkImageLoader(
imageUrl: Config.signIcon,
borderRadius: 0,
placeHolderWidget: const SizedBox(),
)
: Image.asset('nav_sign.webp'.homePath),
),
);
}
}
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hjsq/assets_tool/images.dart';
import 'package:hjsq/hj_utils/screen.dart';
import 'package:hjsq/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, color: Color(0xFFFFC000)),
6.sizeBoxW,
Text(
'片库',
style: TextStyle(
color: Color(0xFFFFC000),
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:hjsq/assets_tool/images.dart';
import 'package:hjsq/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:hjsq/config/config.dart';
import 'package:hjsq/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:hjsq/assets_tool/app_colors.dart';
import 'package:hjsq/hj_model/video_model.dart';
import 'package:hjsq/hj_utils/screen.dart';
import 'package:hjsq/tools_base/widget/net_image_widget.dart';
import '../../../community/community_tag_page/community_tag_page.dart';
import 'package:hjsq/extension/extensions.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,
),
),
),
],
),
),
);
}
}