flutter/lib/pages/search/search-result.dart
2025-09-20 11:21:32 +08:00

1005 lines
31 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_service.dart' hide logger;
import 'package:loopin/api/common_api.dart';
import 'package:loopin/components/my_toast.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
import '../../behavior/custom_scroll_behavior.dart';
class SearchResultPage extends StatefulWidget {
const SearchResultPage({super.key});
@override
State<SearchResultPage> createState() => _SearchResultPageState();
}
class _SearchResultPageState extends State<SearchResultPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
String _searchQuery = Get.arguments?['searchWords'] ?? '';
int _initialTabIndex = 0;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
// 分页参数
static const int _pageSize = 20;
int _videoCurrentPage = 1;
int _productCurrentPage = 1;
int _userCurrentPage = 1;
bool _videoHasMore = true;
bool _productHasMore = true;
bool _userHasMore = true;
// 三个tab的数据
List<dynamic> _videoResults = [];
List<dynamic> _productResults = [];
List<dynamic> _userResults = [];
bool _isLoading = true;
bool _isLoadingMore = false;
// 统一的高度常量
static const double _itemCornerRadius = 8;
@override
void initState() {
super.initState();
// 初始化搜索控制器
_searchController.text = _searchQuery;
// 解析tab参数
final tabParam = Get.arguments?['tab'];
if (tabParam != null) {
_initialTabIndex = tabParam ?? 0;
}
_tabController = TabController(
length: 3,
vsync: this,
initialIndex: _initialTabIndex,
);
// 监听Tab切换
_tabController.addListener(_onTabChanged);
// 加载初始数据
_loadInitialData();
}
@override
void dispose() {
_tabController.removeListener(_onTabChanged);
_tabController.dispose();
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
void _onTabChanged() {
if (_tabController.indexIsChanging) {
// 加载对应Tab的数据
_loadTabData(_tabController.index, isRefresh: true);
}
}
// 执行搜索
void _performSearch() {
final newQuery = _searchController.text.trim();
if (newQuery.isEmpty) {
MyToast().tip(
title: '请输入搜索关键词',
position: 'center',
type: 'warning',
);
return;
}
if (newQuery == _searchQuery) {
return; // 搜索关键词相同,不需要重新搜索
}
setState(() {
_searchQuery = newQuery;
_isLoading = true;
// 重置分页状态
_resetPagination();
});
// 关闭键盘
_searchFocusNode.unfocus();
// 重新加载数据
_loadInitialData();
}
// 重置分页状态
void _resetPagination() {
_videoCurrentPage = 1;
_productCurrentPage = 1;
_userCurrentPage = 1;
_videoHasMore = true;
_productHasMore = true;
_userHasMore = true;
_videoResults.clear();
_productResults.clear();
_userResults.clear();
}
// 加载初始数据
Future<void> _loadInitialData() async {
try {
setState(() {
_isLoading = true;
});
// 加载当前Tab的数据
await _loadTabData(_tabController.index, isRefresh: true);
} catch (e) {
print('加载数据失败: $e');
MyToast().tip(
title: '加载失败',
position: 'center',
type: 'error',
);
} finally {
setState(() {
_isLoading = false;
});
}
}
// 加载指定Tab的数据
Future<void> _loadTabData(int tabIndex, {bool isRefresh = false}) async {
try {
if (isRefresh) {
setState(() {
_isLoading = true;
});
} else {
setState(() {
_isLoadingMore = true;
});
}
int currentPage;
List<dynamic> currentResults;
bool hasMore;
switch (tabIndex) {
case 0: // 视频
currentPage = isRefresh ? 1 : _videoCurrentPage + 1;
currentResults = _videoResults;
hasMore = _videoHasMore;
break;
case 1: // 商品
currentPage = isRefresh ? 1 : _productCurrentPage + 1;
currentResults = _productResults;
hasMore = _productHasMore;
break;
case 2: // 用户
currentPage = isRefresh ? 1 : _userCurrentPage + 1;
currentResults = _userResults;
hasMore = _userHasMore;
break;
default:
return;
}
// 如果没有更多数据,直接返回
if (!hasMore && !isRefresh) {
setState(() {
_isLoadingMore = false;
});
return;
}
final data = {
'title': _searchQuery,
'size': _pageSize,
'type': tabIndex + 1, // 1-视频2-商品3-用户
'current': currentPage,
};
final res = await Http.post(CommonApi.aggregationSearchApi, data: data);
if (res['code'] == 200) {
final newData = res['data']['records'] ?? [];
final total = res['data']['total'] ?? 0;
print('搜索数据结果$newData');
print('搜索数据参数$data');
setState(() {
switch (tabIndex) {
case 0:
if (isRefresh) {
_videoResults = newData;
_videoCurrentPage = 1;
} else {
_videoResults.addAll(newData);
_videoCurrentPage = currentPage;
}
_videoHasMore = _videoResults.length < total;
break;
case 1:
if (isRefresh) {
_productResults = newData;
_productCurrentPage = 1;
} else {
_productResults.addAll(newData);
_productCurrentPage = currentPage;
}
_productHasMore = _productResults.length < total;
break;
case 2:
if (isRefresh) {
_userResults = newData;
_userCurrentPage = 1;
} else {
_userResults.addAll(newData);
_userCurrentPage = currentPage;
}
_userHasMore = _userResults.length < total;
break;
}
});
}
} catch (e) {
print('加载Tab数据失败: $e');
if (!isRefresh) {
MyToast().tip(
title: '加载更多失败',
position: 'center',
type: 'error',
);
}
} finally {
setState(() {
if (isRefresh) {
_isLoading = false;
} else {
_isLoadingMore = false;
}
});
}
}
// 加载更多数据
void _loadMoreData(int tabIndex) {
if (_isLoadingMore) return;
_loadTabData(tabIndex, isRefresh: false);
}
// 点击关注按钮
onFocusBtnClick(user, index) async {
final vlogerId = user['id'];
final doIFollowVloger = user['doIFollowVloger'];
print('是否关注此用户------------->$doIFollowVloger');
print('此用户UserId------------->$vlogerId');
if (doIFollowVloger == false || doIFollowVloger == null) {
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
print('关注结果------------->${res.success}');
if (res.success) {
setState(() {
_userResults[index]['doIFollowVloger'] = true;
});
}
} else {
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
print('取消关注结果------------->${res.success}');
if (res.success) {
setState(() {
_userResults[index]['doIFollowVloger'] = false;
});
}
}
}
// 构建加载更多组件
Widget _buildLoadMoreWidget(int tabIndex) {
if (_isLoadingMore) {
return const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
bool hasMore;
switch (tabIndex) {
case 0:
hasMore = _videoHasMore;
break;
case 1:
hasMore = _productHasMore;
break;
case 2:
hasMore = _userHasMore;
break;
default:
hasMore = false;
}
if (!hasMore && _getCurrentResults(tabIndex).isNotEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距
child: Text(
'没有更多数据了',
style: TextStyle(
fontSize: 12, // 减小字体
color: Colors.grey,
),
),
),
);
}
return Container();
}
// 获取当前Tab的数据
List<dynamic> _getCurrentResults(int tabIndex) {
switch (tabIndex) {
case 0:
return _videoResults;
case 1:
return _productResults;
case 2:
return _userResults;
default:
return [];
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Container(
height: 36,
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
const SizedBox(width: 12),
const Icon(Icons.search, color: Colors.grey, size: 20),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchController,
focusNode: _searchFocusNode,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.2,
),
decoration: const InputDecoration(
hintText: '搜索视频、商品、用户、团购...',
hintStyle: TextStyle(
color: Colors.grey,
fontSize: 14,
height: 1.2,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 8),
isDense: true,
),
onSubmitted: (_) => _performSearch(),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.grey, size: 18),
onPressed: () {
_searchController.clear();
},
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
const SizedBox(width: 4),
],
),
),
backgroundColor: Colors.black,
foregroundColor: Colors.white,
elevation: 0.5,
actions: [
TextButton(
onPressed: _performSearch,
child: const Text(
'搜索',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
],
),
body: Column(
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.only(top: 8),
child: TabBar(
controller: _tabController,
indicatorColor: Colors.pink,
labelColor: Colors.pink,
unselectedLabelColor: Colors.grey,
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.normal),
padding: const EdgeInsets.only(left: 16),
indicatorPadding: EdgeInsets.zero,
labelPadding: const EdgeInsets.symmetric(horizontal: 16),
indicatorSize: TabBarIndicatorSize.label,
dividerColor: Colors.transparent,
tabs: const [
Tab(text: '视频'),
Tab(text: '商品'),
Tab(text: '用户'),
],
),
),
// Tab内容区域
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: TabBarView(
controller: _tabController,
children: [
// 视频Tab
_buildVideoTab(),
// 商品Tab
_buildProductTab(),
// 用户Tab
_buildUserTab(),
],
),
),
],
),
);
}
// 视频Tab
Widget _buildVideoTab() {
if (_videoResults.isEmpty && !_isLoading) {
return _buildEmptyView('暂无视频结果');
}
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _videoHasMore) {
_loadMoreData(0);
}
return false;
},
child: Column(
children: [
Expanded(
child: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.65,
),
itemCount: _videoResults.length,
itemBuilder: (context, index) {
final video = _videoResults[index] as Map<String, dynamic>;
return _buildVideoItem(video);
},
),
),
),
_buildVideoLoadMoreWidget(),
],
),
);
}
// 视频项构建
Widget _buildVideoItem(Map<String, dynamic> video) {
return GestureDetector(
onTap: () {
// 视频点击事件处理
print('点击了视频: ${video['id']}');
Get.toNamed('/videoDetail', arguments: {'videoId': video['id']});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_itemCornerRadius),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 视频封面 - 自适应高度
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(_itemCornerRadius),
topRight: Radius.circular(_itemCornerRadius),
),
child: AspectRatio(
aspectRatio: 0.8, // 保持1:1.2的宽高比
child: Container(
color: Colors.grey[200],
child: video['firstFrameImg'] != null && video['firstFrameImg'].toString().isNotEmpty
? Image.network(
video['firstFrameImg'].toString(),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.videocam,
color: Colors.grey,
size: 32,
),
),
);
},
)
: Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.videocam,
color: Colors.grey,
size: 32,
),
),
),
),
),
),
// 使用Expanded将信息区域推到底部
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 标题在上,用户信息在下
children: [
// 标题区域 - 修改为最多1行
Text(
video['title']?.toString() ?? '无标题',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
height: 1.2,
),
maxLines: 1, // 修改为1行
overflow: TextOverflow.ellipsis,
),
// 用户信息区域 - 固定在底部
Row(
children: [
// 用户头像
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
image: video['avatar'] != null && video['avatar'].toString().isNotEmpty
? DecorationImage(
image: NetworkImage(video['avatar'].toString()),
fit: BoxFit.cover,
)
: null,
),
child: video['avatar'] == null || video['avatar'].toString().isEmpty
? const Icon(Icons.person, size: 10, color: Colors.grey)
: null,
),
const SizedBox(width: 6),
// 用户名
Expanded(
child: Text(
video['nickname']?.toString() ?? '未知作者',
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// 点赞数
Row(
children: [
const Icon(Icons.favorite_border, size: 10, color: Colors.grey),
const SizedBox(width: 2),
Text(
Utils.graceNumber(video['likeCounts'] ?? 0),
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
),
],
),
],
),
],
),
),
),
],
),
),
);
}
// 视频加载更多组件
Widget _buildVideoLoadMoreWidget() {
if (_isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
if (!_videoHasMore && _videoResults.isNotEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Text(
'没有更多数据了',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
);
}
return Container();
}
// 商品Tab
Widget _buildProductTab() {
if (_productResults.isEmpty && !_isLoading) {
return _buildEmptyView('暂无商品结果');
}
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _productHasMore) {
_loadMoreData(1);
}
return false;
},
child: Column(
children: [
Expanded(
child: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.65, // 调整为与视频相同的宽高比
),
itemCount: _productResults.length,
itemBuilder: (context, index) {
final product = _productResults[index] as Map<String, dynamic>;
return _buildProductItem(product);
},
),
),
),
_buildProductLoadMoreWidget(),
],
),
);
}
// 商品加载更多组件
Widget _buildProductLoadMoreWidget() {
if (_isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
if (!_productHasMore && _productResults.isNotEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Text(
'没有更多数据了',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
);
}
return Container();
}
Widget _buildProductItem(Map<String, dynamic> product) {
return GestureDetector(
onTap: () {
// 商品点击事件处理
print('点击了商品: ${product['id']}');
Get.toNamed('/goods', arguments: {'goodsId': product['id']});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_itemCornerRadius),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 商品图片 - 使用AspectRatio保持固定宽高比与视频一致
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(_itemCornerRadius),
topRight: Radius.circular(_itemCornerRadius),
),
child: AspectRatio(
aspectRatio: 0.8, // 保持与视频相同的宽高比
child: Container(
color: Colors.grey[200],
child: product['pic'] != null && product['pic'].toString().isNotEmpty
? Image.network(
product['pic'].toString(),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.shopping_bag,
color: Colors.grey,
size: 32,
),
),
);
},
)
: Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.shopping_bag,
color: Colors.grey,
size: 32,
),
),
),
),
),
),
// 商品信息
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 商品名称
Text(
product['name']?.toString() ?? '未知商品',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 价格和销量信息
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'¥${product['price']?.toString() ?? '0.00'}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.pink,
),
),
Text(
'已售 ${Utils.graceNumber(product['sales'] ?? 0)}',
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
),
],
),
],
),
),
),
],
),
),
);
}
// 用户Tab
Widget _buildUserTab() {
if (_userResults.isEmpty && !_isLoading) {
return _buildEmptyView('暂无用户结果');
}
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _userHasMore) {
_loadMoreData(2);
}
return false;
},
child: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _userResults.length + 1,
itemBuilder: (context, index) {
if (index == _userResults.length) {
return _buildLoadMoreWidget(2);
}
final user = _userResults[index] as Map<String, dynamic>;
return _buildUserItem(user, index);
},
),
),
);
}
Widget _buildUserItem(Map<String, dynamic> user, int index) {
// 判断当前用户是否已被关注
bool isFollowing = user['doIFollowVloger'] ?? false;
print('111111111111111111111111$isFollowing');
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[200],
image: user['avatar'] != null
? DecorationImage(
image: NetworkImage(user['avatar'].toString()),
fit: BoxFit.cover,
)
: null,
),
child: user['avatar'] == null ? const Icon(Icons.person, color: Colors.grey, size: 20) : null,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user['nickname']?.toString() ?? '未知用户',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'粉丝: ${user['fansCount']?.toString() ?? '0'}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
),
ElevatedButton(
onPressed: () async {
await onFocusBtnClick(user, index);
},
style: ElevatedButton.styleFrom(
backgroundColor: isFollowing ? Colors.grey[300] : Colors.pink,
foregroundColor: isFollowing ? Colors.grey[600] : Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
// 添加边框样式
side: isFollowing ? BorderSide(color: Colors.grey[400]!, width: 0.5) : BorderSide.none,
),
child: Text(
isFollowing ? '已关注' : '关注',
style: const TextStyle(fontSize: 11),
),
),
],
),
);
}
// 空视图
Widget _buildEmptyView(String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 50, color: Colors.grey),
const SizedBox(height: 12),
Text(
message,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
);
}
}