diff --git a/lib/pages/search/index.dart b/lib/pages/search/index.dart new file mode 100644 index 0000000..a41ecb2 --- /dev/null +++ b/lib/pages/search/index.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:loopin/components/my_toast.dart'; +import '../../behavior/custom_scroll_behavior.dart'; + +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + final TextEditingController _searchController = TextEditingController(); + final GetStorage _storage = GetStorage(); + List _searchHistory = []; + @override + void initState() { + super.initState(); + _loadSearchHistory(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + // 加载搜索历史 + void _loadSearchHistory() { + setState(() { + _searchHistory = _storage.read('searchHistory') ?? []; + }); + } + + // 保存搜索历史 + void _saveSearchHistory() { + _storage.write('searchHistory', _searchHistory); + } + + // 添加搜索项 + void _addSearchItem(String query) { + if (query.trim().isEmpty) return; + + setState(() { + // 移除重复项 + _searchHistory.remove(query); + // 添加到开头 + _searchHistory.insert(0, query); + // 暂时限制历史记录数量未10条 + if (_searchHistory.length > 10) { + _searchHistory = _searchHistory.sublist(0, 10); + } + }); + _saveSearchHistory(); + } + + // 清除单个搜索历史 + void _removeSearchItem(int index) { + setState(() { + _searchHistory.removeAt(index); + }); + _saveSearchHistory(); + } + + // 清除所有搜索历史 + void _clearAllHistory() { + setState(() { + _searchHistory.clear(); + }); + _saveSearchHistory(); + + MyToast().tip( + title: '搜索历史已清除', + position: 'center', + type: 'success', + ); + } + + // 执行搜索 + void _performSearch() { + final searchWords = _searchController.text.trim(); + if (searchWords.isNotEmpty) { + _addSearchItem(searchWords); + _searchController.clear(); + FocusScope.of(context).unfocus(); + // 去搜索结果页,支持带着搜索文字和搜索tab索引 + Get.toNamed('/search-result', arguments: searchWords); + } else { + MyToast().tip( + title: '请输入搜索内容', + position: 'center', + type: 'error', + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + centerTitle: true, + backgroundColor: Colors.black, + foregroundColor: Colors.white, + elevation: 0, + title: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(20), + ), + child: TextField( + controller: _searchController, + style: const TextStyle(color: Colors.white, fontSize: 16), + decoration: InputDecoration( + hintText: '请输入内容~', + hintStyle: TextStyle(color: Colors.grey[500], fontSize: 16), + border: InputBorder.none, + prefixIcon: Icon(Icons.search, color: Colors.grey[500], size: 20), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: Icon(Icons.close, color: Colors.grey[500], size: 20), + onPressed: () { + _searchController.clear(); + setState(() {}); + }, + ) + : null, + contentPadding: const EdgeInsets.symmetric(vertical: 10), + ), + onSubmitted: (_) => _performSearch(), + onChanged: (_) => setState(() {}), + ), + ), + actions: [ + TextButton( + onPressed: _performSearch, + child: const Text('搜索', style: TextStyle(color: Colors.white, fontSize: 16)), + ), + ], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 搜索历史 + if (_searchHistory.isNotEmpty) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '搜索历史', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold + ), + ), + TextButton( + onPressed: _clearAllHistory, + child: const Text( + '清除所有', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(_searchHistory.length, (index) { + return GestureDetector( + onTap: () { + _searchController.text = _searchHistory[index]; + _performSearch(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _searchHistory[index], + style: const TextStyle(color: Colors.white), + ), + const SizedBox(width: 6), + GestureDetector( + onTap: () => _removeSearchItem(index), + child: Icon(Icons.close, size: 16, color: Colors.grey[500]), + ), + ], + ), + ), + ); + }), + ), + const SizedBox(height: 24), + ], + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/search/search-result.dart b/lib/pages/search/search-result.dart new file mode 100644 index 0000000..08e384c --- /dev/null +++ b/lib/pages/search/search-result.dart @@ -0,0 +1,622 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/service/http.dart'; +import 'package:loopin/components/my_toast.dart'; +import '../../behavior/custom_scroll_behavior.dart'; + +class SearchResultPage extends StatefulWidget { + const SearchResultPage({super.key}); + + @override + State createState() => _SearchResultPageState(); +} + +class _SearchResultPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + String _searchQuery = Get.parameters?['searchWords'] ?? ''; + int _initialTabIndex = 0; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + + // 三个tab的数据 + List _videoResults = []; + List _productResults = []; + List _userResults = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + + // 初始化搜索控制器 + _searchController.text = _searchQuery; + + // 解析tab参数 + final tabParam = Get.parameters?['tab']; + if (tabParam != null && tabParam.isNotEmpty) { + _initialTabIndex = int.tryParse(tabParam) ?? 0; + } + + _tabController = TabController( + length: 4, + 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); + } + } + + // 执行搜索 + 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; + }); + + // 关闭键盘 + _searchFocusNode.unfocus(); + + // 重新加载数据 + _loadInitialData(); + } + + // 加载初始数据 + Future _loadInitialData() async { + try { + setState(() { + _isLoading = true; + }); + + // 加载第一个Tab的数据 + await _loadTabData(_initialTabIndex); + } catch (e) { + print('加载数据失败: $e'); + MyToast().tip( + title: '加载失败', + position: 'center', + type: 'error', + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + // 加载指定Tab的数据 + Future _loadTabData(int tabIndex) async { + try { + setState(() { + _isLoading = true; + }); + + switch (tabIndex) { + case 0: // 视频 + final res = await Http.get('/api/search/videos', params: { + 'keyword': _searchQuery, + 'page': 1, + 'current': 20, + }); + if (res['code'] == 200) { + setState(() { + _videoResults = res['data']['list'] ?? []; + }); + } + break; + case 1: // 商品 + final res = await Http.get('/api/search/products', params: { + 'keyword': _searchQuery, + 'page': 1, + 'current': 20, + }); + if (res['code'] == 200) { + setState(() { + _productResults = res['data']['list'] ?? []; + }); + } + break; + case 2: // 用户 + final res = await Http.get('/api/search/users', params: { + 'keyword': _searchQuery, + 'page': 1, + 'current': 20, + }); + if (res['code'] == 200) { + setState(() { + _userResults = res['data']['list'] ?? []; + }); + } + break; + } + } catch (e) { + print('加载Tab数据失败: $e'); + MyToast().tip( + title: '加载失败', + position: 'center', + type: 'error', + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @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, + // isScrollable: true, + 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, // 改为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) { + return _buildEmptyView('暂无视频结果'); + } + + return ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: _videoResults.length, + itemBuilder: (context, index) { + final video = _videoResults[index] as Map; + return _buildVideoItem(video); + }, + ), + ); + } + + Widget _buildVideoItem(Map video) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 视频封面 + Container( + width: 120, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Colors.grey[200], + image: video['cover'] != null + ? DecorationImage( + image: NetworkImage(video['cover'].toString()), + fit: BoxFit.cover, + ) + : null, + ), + child: video['cover'] == null + ? const Icon(Icons.videocam, color: Colors.grey) + : null, + ), + const SizedBox(width: 12), + // 视频信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + video['title']?.toString() ?? '无标题', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + video['author']?.toString() ?? '未知作者', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + '#${video['tag']?.toString() ?? '无标签'}', + style: const TextStyle( + fontSize: 12, + color: Colors.blue, + ), + ), + const SizedBox(width: 8), + Text( + video['location']?.toString() ?? '', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + // 商品Tab + Widget _buildProductTab() { + if (_productResults.isEmpty) { + return _buildEmptyView('暂无商品结果'); + } + + return ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: _productResults.length, + itemBuilder: (context, index) { + final product = _productResults[index] as Map; + return _buildProductItem(product); + }, + ), + ); + } + + Widget _buildProductItem(Map product) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + 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: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Colors.grey[200], + image: product['image'] != null + ? DecorationImage( + image: NetworkImage(product['image'].toString()), + fit: BoxFit.cover, + ) + : null, + ), + child: product['image'] == null + ? const Icon(Icons.shopping_bag, color: Colors.grey) + : null, + ), + const SizedBox(width: 12), + // 商品信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product['name']?.toString() ?? '未知商品', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + '¥${product['price']?.toString() ?? '0.00'}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.pink, + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Text( + '已售 ${product['sold']?.toString() ?? '0'}', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(width: 12), + Text( + '${product['comments']?.toString() ?? '0'}条评论', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + // 购买按钮 + IconButton( + onPressed: () { + // 跳转到商品详情 + }, + icon: const Icon(Icons.shopping_cart, size: 20), + ), + ], + ), + ); + } + + // 用户Tab + Widget _buildUserTab() { + if (_userResults.isEmpty) { + return _buildEmptyView('暂无用户结果'); + } + + return ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: _userResults.length, + itemBuilder: (context, index) { + final user = _userResults[index] as Map; + return _buildUserItem(user); + }, + ), + ); + } + + Widget _buildUserItem(Map user) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + 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: 50, + height: 50, + 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) + : null, + ), + const SizedBox(width: 12), + // 用户信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user['name']?.toString() ?? '未知用户', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + '粉丝: ${user['fans']?.toString() ?? '0'}', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + // 关注按钮 + ElevatedButton( + onPressed: () { + // 关注用户 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pink, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text( + '关注', + style: TextStyle(fontSize: 12), + ), + ), + ], + ) + ); + } + + // 空视图 + Widget _buildEmptyView(String message) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.search_off, size: 60, color: Colors.grey), + const SizedBox(height: 16), + Text( + message, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/video/index.dart b/lib/pages/video/index.dart index da6c742..74c0cfb 100644 --- a/lib/pages/video/index.dart +++ b/lib/pages/video/index.dart @@ -128,7 +128,9 @@ class _VideoPageState extends State with SingleTickerProviderStateMix Icons.search_rounded, color: tabColor(), ), - onPressed: () {}, + onPressed: () { + Get.toNamed('/search'); + }, ), ), ], diff --git a/lib/pages/video/module/attention.dart b/lib/pages/video/module/attention.dart index 0fe12c5..4417eb2 100644 --- a/lib/pages/video/module/attention.dart +++ b/lib/pages/video/module/attention.dart @@ -5,16 +5,20 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_message.dart'; +import 'package:loopin/IM/im_service.dart' hide logger; import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; +import 'package:loopin/utils/download_video.dart'; +import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; @@ -46,15 +50,505 @@ class AttentionModule extends StatefulWidget { State createState() => _AttentionModuleState(); } +class CommentBottomSheet extends StatefulWidget { + final String videoId; + final Function(int) onCommentCountChanged; // 新增回调函数 + + const CommentBottomSheet({ + super.key, + required this.videoId, + required this.onCommentCountChanged, // 接收回调 + }); + + @override + _CommentBottomSheetState createState() => _CommentBottomSheetState(); +} + +class _CommentBottomSheetState extends State { + // 评论相关状态(移动到内部) + int commentPage = 1; + final int commentPageSize = 10; + bool isLoadingMoreComments = false; + bool hasMoreComments = true; + List> commentList = []; + String replyingCommentId = ''; + String replyingCommentUser = ''; + + // 新增:子评论相关状态 + Map expandedReplies = {}; // 存储已展开的回复 {commentId: true/false} + Map>> replyData = {}; // 存储子评论数据 {commentId: [replies]} + Map replyPage = {}; // 子评论分页 + Map isLoadingReplies = {}; // 子评论加载状态 + + @override + void initState() { + super.initState(); + // 初始化时加载评论 + fetchComments(false); + } + + Future fetchComments(bool loadMore) async { + if (isLoadingMoreComments && !loadMore) return; + + setState(() { + isLoadingMoreComments = true; + }); + + if (!loadMore) { + commentPage = 1; + hasMoreComments = true; + commentList.clear(); + } + logger.d('入参vlogId-------------------->: ${widget.videoId}'); + try { + final res = await Http.post(VideoApi.videoCommentList, data: { + 'vlogId': widget.videoId, + 'current': commentPage, + 'size': commentPageSize, + }); + + logger.d('评论接口列表返回: ${json.encode(res)}'); + + if (res['code'] == 200 && res['data'] != null) { + final data = res['data']; + final List> newComments = List>.from(data['records'] ?? []); + final int total = data['total'] ?? 0; + + setState(() { + if (loadMore) { + commentList.addAll(newComments); + } else { + commentList = newComments; + } + hasMoreComments = commentList.length < total; + commentPage++; + }); + widget.onCommentCountChanged(total); + logger.d('成功加载 ${newComments.length} 条评论,总共 $total 条'); + } + } catch (e) { + logger.e('获取评论异常: $e'); + } finally { + setState(() { + isLoadingMoreComments = false; + }); + } + } + + Future postComment(String content, {String parentCommentId = ''}) async { + try { + final res = await Http.post(VideoApi.doVideoComment, data: { + 'vlogId': widget.videoId, + 'content': content, + 'fatherCommentId': parentCommentId.isNotEmpty ? parentCommentId : null, + }); + + if (res['code'] == 200) { + // 如果是回复,刷新对应的子评论 + if (parentCommentId.isNotEmpty) { + fetchReplies(parentCommentId, false); + // 更新主评论的子评论数量 + setState(() { + final comment = commentList.firstWhere((c) => c['id'] == parentCommentId, orElse: () => {}); + if (comment.isNotEmpty) { + comment['childCount'] = (comment['childCount'] ?? 0) + 1; + } + }); + } else { + // 如果是新评论,刷新整个列表 + fetchComments(false); + } + widget.onCommentCountChanged(commentList.length + 1); + MyToast().tip( + title: '评论成功', + position: 'center', + type: 'success', + ); + } + } catch (e) { + logger.i('发布评论失败: $e'); + MyToast().tip( + title: '评论失败', + position: 'center', + type: 'error', + ); + } + } + + // 获取二级评论,复用一级评论的接口,修改传参 + Future fetchReplies(String commentId, bool loadMore) async { + if (isLoadingReplies[commentId] == true && !loadMore) return; + + setState(() { + isLoadingReplies[commentId] = true; + }); + + if (!loadMore) { + replyPage[commentId] = 1; + replyData[commentId] = []; + } + + try { + final res = await Http.post(VideoApi.videoCommentList, data: { + 'fatherCommentId': commentId, + 'current': replyPage[commentId], + 'size': commentPageSize, + }); + + if (res['code'] == 200 && res['data'] != null) { + final data = res['data']; + final List> newReplies = List>.from(data['records'] ?? []); + + setState(() { + if (loadMore) { + replyData[commentId]!.addAll(newReplies); + } else { + replyData[commentId] = newReplies; + } + replyPage[commentId] = replyPage[commentId]! + 1; + }); + } + } catch (e) { + logger.e('获取子评论异常: $e'); + } finally { + setState(() { + isLoadingReplies[commentId] = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.white, + child: Column( + children: [ + Container( + padding: EdgeInsets.fromLTRB(15.0, 10.0, 10.0, 5.0), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFFAFAFA)))), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + /* Expanded( + child: Text.rich(TextSpan(children: [ + TextSpan( + text: '大家都在搜: ', + style: TextStyle(color: Colors.grey), + ), + TextSpan( + text: '黑神话-悟空', + style: TextStyle(color: const Color(0xFF496D80)), + ), + ]))),*/ + GestureDetector( + child: Container( + height: 22.0, + width: 22.0, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(100.0), + ), + child: UnconstrainedBox(child: Icon(Icons.close, color: Colors.black45, size: 14.0))), + onTap: () { + Navigator.pop(context); + }, + ), + ], + ), + Text( + '${commentList.length}条评论', + style: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w600), + ), + ], + ), + ), + Expanded( + child: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !isLoadingMoreComments && hasMoreComments) { + fetchComments(true); + } + return false; + }, + child: commentList.isEmpty && !isLoadingMoreComments + ? Center( + child: Text( + '暂无评论', + style: TextStyle(color: Colors.grey), + ), + ) + : ListView.builder( + physics: BouncingScrollPhysics(), + itemCount: commentList.length + (hasMoreComments ? 1 : 0), + itemBuilder: (context, index) { + if (index == commentList.length) { + return Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: isLoadingMoreComments ? CircularProgressIndicator() : Text('没有更多评论了'), + ), + ); + } + + final comment = commentList[index]; + final hasReplies = comment['childCount'] > 0; + final isExpanded = expandedReplies[comment['id']] == true; + final replies = replyData[comment['id']] ?? []; + + return Column( + children: [ + // 主评论 + ListTile( + isThreeLine: true, + leading: ClipRRect( + borderRadius: BorderRadius.circular(50.0), + child: NetworkOrAssetImage( + imageUrl: comment['commentUserFace'] ?? 'assets/images/avatar/default.png', + width: 30.0, + height: 30.0, + fit: BoxFit.cover, + ), + ), + title: Row( + children: [ + Expanded( + child: Text( + comment['commentUserNickname'] ?? '未知用户', + style: TextStyle( + color: Colors.grey, + fontSize: 12.0, + ), + ), + ), + SizedBox(width: 20.0), + GestureDetector( + onTap: () { + setState(() { + replyingCommentId = comment['id']; + replyingCommentUser = comment['commentUserNickname'] ?? '未知用户'; + }); + }, + child: Icon( + Icons.reply, + color: Colors.black54, + size: 16.0, + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.symmetric(vertical: 5.0), + child: Text( + comment['content'] ?? '', + style: TextStyle( + fontSize: 14.0, + ), + ), + ), + if (hasReplies) + GestureDetector( + onTap: () { + setState(() { + expandedReplies[comment['id']] = !isExpanded; + if (expandedReplies[comment['id']] == true && + (replyData[comment['id']] == null || replyData[comment['id']]!.isEmpty)) { + fetchReplies(comment['id'], false); + } + }); + }, + child: Container( + margin: EdgeInsets.only(right: 15.0), + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(20.0), + ), + child: Row(children: [ + Text( + '${comment['childCount']}回复', + style: TextStyle(fontSize: 12.0), + ), + Icon( + isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down, + size: 16.0, + ) + ]), + ), + ), + Text( + comment['createTime']?.toString().substring(0, 10) ?? '', + style: TextStyle(color: Colors.grey, fontSize: 12.0), + ), + ], + ), + ), + + // 子评论列表 + if (isExpanded && replies.isNotEmpty) + Padding( + padding: EdgeInsets.only(left: 40.0), + child: Column( + children: [ + ...replies.map((reply) => ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(25.0), + child: NetworkOrAssetImage( + imageUrl: reply['commentUserFace'] ?? 'assets/images/avatar/default.png', + width: 25.0, + height: 25.0, + fit: BoxFit.cover, + ), + ), + title: Row( + children: [ + Expanded( + child: Text( + reply['commentUserNickname'] ?? '未知用户', + style: TextStyle( + color: Colors.grey, + fontSize: 11.0, + ), + ), + ), + GestureDetector( + onTap: () { + setState(() { + replyingCommentId = comment['id']; + replyingCommentUser = reply['commentUserNickname'] ?? '未知用户'; + }); + }, + child: Icon( + Icons.reply, + color: Colors.black54, + size: 14.0, + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.symmetric(vertical: 3.0), + child: Text( + reply['content'] ?? '', + style: TextStyle(fontSize: 13.0), + ), + ), + Text( + reply['createTime']?.toString().substring(0, 10) ?? '', + style: TextStyle(color: Colors.grey, fontSize: 11.0), + ), + ], + ), + )), + + // 加载更多子评论按钮 + if (replies.length < comment['childCount']) + Center( + child: TextButton( + onPressed: () => fetchReplies(comment['id'], true), + child: isLoadingReplies[comment['id']] == true ? CircularProgressIndicator() : Text('加载更多回复'), + ), + ), + ], + ), + ), + + Divider(height: 1, color: Colors.grey[200]), + ], + ); + }, + ), + ), + ), + // 在回复输入区域显示更详细的信息 + if (replyingCommentId.isNotEmpty) + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + color: Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + '回复 @$replyingCommentUser', + style: TextStyle(fontSize: 12.0, color: Colors.blue), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () { + setState(() { + replyingCommentId = ''; + replyingCommentUser = ''; + }); + }, + child: Icon(Icons.close, size: 16.0), + ), + ], + ), + ), + GestureDetector( + child: Container( + margin: EdgeInsets.all(10.0), + height: 40.0, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(30.0), + ), + child: Row( + children: [ + SizedBox(width: 15.0), + Icon( + Icons.edit_note, + color: Colors.black54, + size: 16.0, + ), + SizedBox(width: 5.0), + Text( + replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', + style: TextStyle(color: Colors.black54, fontSize: 14.0), + ), + ], + ), + ), + onTap: () { + Navigator.push( + context, + FadeRoute( + child: PopupReply( + hintText: replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', + onChanged: (value) { + debugPrint('评论内容: $value'); + }, + onSubmitted: (value) { + if (value.isNotEmpty) { + postComment(value, parentCommentId: replyingCommentId); + setState(() { + replyingCommentId = ''; + replyingCommentUser = ''; + }); + Navigator.pop(context); + } + }, + ))); + }, + ), + ], + ), + ); + } +} + class _AttentionModuleState extends State { VideoModuleController videoModuleController = Get.put(VideoModuleController()); final ChatController chatController = Get.find(); - -// class _RecommendModuleState extends State with AutomaticKeepAliveClientMixin { -// @override -// bool get wantKeepAlive => true; -// VideoModuleController videoModuleController = Get.find(); - + final RxMap lastReturnData = RxMap({}); // 分页内容 int page = 1; final int pageSize = 10; @@ -78,41 +572,36 @@ class _AttentionModuleState extends State { late Duration duration = Duration.zero; // 总时长 // 视频数据 List videoList = []; - // 评论数据 - List commentList = [ - {'avatar': 'assets/images/avatar/img01.jpg', 'name': 'Alice', 'desc': '用汗水浇灌希望,让努力铸就辉煌,你付出的每一刻,都是在靠近成功的彼岸。'}, - {'avatar': 'assets/images/avatar/img02.jpg', 'name': '悟空', 'desc': '黑暗遮不住破晓的曙光,困境困不住奋进的脚步,勇往直前,你定能冲破阴霾。'}, - {'avatar': 'assets/images/avatar/img03.jpg', 'name': '木棉花', 'desc': '每一次跌倒都是为了下一次更有力地跃起,别放弃~!'}, - {'avatar': 'assets/images/avatar/img04.jpg', 'name': '狗仔', 'desc': '人生没有白走的路,每一步都算数,那些辛苦的过往,会在未来化作最美的勋章。'}, - {'avatar': 'assets/images/avatar/img05.jpg', 'name': '向日葵', 'desc': '以梦为马,不负韶华,握紧手中的笔,书写属于自己的热血传奇,让青春绽放光芒。'}, - {'avatar': 'assets/images/avatar/img06.jpg', 'name': '健身女神', 'desc': '哪怕身处谷底,只要抬头仰望,便能看见漫天繁星,心怀希望,就能找到出路,奔赴美好。'}, - ]; + + // 评论相关状态 + int commentPage = 1; + final int commentPageSize = 10; + bool isLoadingMoreComments = false; + bool hasMoreComments = true; + List> commentList = []; + int currentVideoId = 0; + String replyingCommentId = ''; // 正在回复的评论ID + String replyingCommentUser = ''; // 正在回复的用户名 + // 分享列表 List shareList = [ {'icon': 'assets/images/share-wx.png', 'label': '好友'}, - {'icon': 'assets/images/share-wx.png', 'label': '微信'}, {'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'}, {'icon': 'assets/images/share-link.png', 'label': '复制链接'}, {'icon': 'assets/images/share-download.png', 'label': '下载'}, - {'icon': 'assets/images/share-download.png', 'label': '下载'}, - {'icon': 'assets/images/share-download.png', 'label': '下载'}, - {'icon': 'assets/images/share-download.png', 'label': '下载'}, - {'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'}, - {'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'}, ]; @override void initState() { super.initState(); -/* videoModuleController.needRefresh.listen((need) { + videoModuleController.needRefresh.listen((need) { if (need) { reInit(); videoModuleController.clearNeedRefresh(); } - });*/ + }); AttentionModule.setPlayer(player); // 获取视频数据 - logger.d('AttentionModule initState1111111111111111111111111111'); fetchVideoList(); } @@ -185,20 +674,15 @@ class _AttentionModuleState extends State { 'current': page, 'size': pageSize, }); - logger.d('关注用户的视频列表:$res'); final data = res['data']; - + logger.d('关注用户的视频列表:$data'); if (data == null || (data is List && data.isEmpty)) { - // MyDialog.toast('没有更多了', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); return; } if (data['records'] is List) { List videos = data['records']; for (var item in videos) { - // print("喜欢:${item['likeCounts']}"); - // print("评论:${item['commentsCounts']}"); - // logger.i(item); item['expanded'] = false; } @@ -214,7 +698,7 @@ class _AttentionModuleState extends State { if (videos.isNotEmpty) { page++; } - logger.i('获取新的视频数据了'); + logger.i('视频数据列表------------------->$videoList'); // 初始化播放器 player.open( @@ -223,6 +707,13 @@ class _AttentionModuleState extends State { ), play: false); player.setPlaylistMode(PlaylistMode.loop); // 循环播放; + + // 第一次加载后播放第一个视频 + if (page == 2 && videoModuleController.videoTabIndex.value == 2 && Get.currentRoute == '/' && videoModuleController.layoutPageCurrent.value == 0) { + player.play(); // 播放第一个 + } else { + logger.i('没播放视频'); + } } } catch (e) { logger.i('获取视频失败: $e'); @@ -231,206 +722,60 @@ class _AttentionModuleState extends State { } } + // 取消喜欢视频 + Future doUnLikeVideo(item) async { + logger.d('点击了点赞按钮$item'); + try { + final res = await Http.post(VideoApi.unlike, data: {'vlogId': item['id']}); + final resCode = res['code']; + if (resCode == 200) { + item['doILikeThisVlog'] = !item['doILikeThisVlog']; + } + } catch (e) { + logger.i('点击取消喜欢按钮报错: $e'); + } + } + + // 点击喜欢视频 + Future doLikeVideo(item) async { + try { + final res = await Http.post(VideoApi.like, data: {'vlogId': item['id']}); + final resCode = res['code']; + if (resCode == 200) { + item['doILikeThisVlog'] = !item['doILikeThisVlog']; + } + logger.i('点赞返回信息----------->: $res'); + } catch (e) { + logger.i('点击喜欢按钮报错: $e'); + } + } + // 评论弹框 void handleComment(index) { - //获取评论数据 + final videoId = videoList[index]['id']; + logger.i('点击了评论按钮$videoId'); showModalBottomSheet( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))), showDragHandle: false, clipBehavior: Clip.antiAlias, - isScrollControlled: true, // 屏幕最大高度 + isScrollControlled: true, constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 3 / 4, // 自定义最大高度 + maxHeight: MediaQuery.of(context).size.height * 3 / 4, ), context: context, builder: (context) { - return Material( - color: Colors.white, - child: Column( - children: [ - Container( - padding: EdgeInsets.fromLTRB(15.0, 10.0, 10.0, 5.0), - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFFAFAFA)))), - child: Column( - spacing: 10.0, - children: [ - Row( - children: [ - Expanded( - child: Text.rich(TextSpan(children: [ - TextSpan( - text: '大家都在搜: ', - style: TextStyle(color: Colors.grey), - ), - TextSpan( - text: '黑神话-悟空', - style: TextStyle(color: const Color(0xFF496D80)), - ), - ]))), - GestureDetector( - child: Container( - height: 22.0, - width: 22.0, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(100.0), - ), - child: UnconstrainedBox(child: Icon(Icons.close, color: Colors.black45, size: 14.0))), - onTap: () { - Get.back(); - }, - ), - ], - ), - Text( - '168条评论', - style: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w600), - ) - ], - ), - ), - Expanded( - child: ScrollConfiguration( - behavior: CustomScrollBehavior().copyWith(scrollbars: false), - child: ListView.builder( - physics: BouncingScrollPhysics(), - shrinkWrap: true, - itemCount: commentList.length, - itemBuilder: (context, index) { - return ListTile( - isThreeLine: true, - leading: ClipRRect( - borderRadius: BorderRadius.circular(50.0), - child: Image.asset( - '${commentList[index]['avatar']}', - width: 30.0, - fit: BoxFit.contain, - ), - ), - title: Row( - children: [ - Expanded( - child: Text( - '${commentList[index]['name']}', - style: TextStyle( - color: Colors.grey, - fontSize: 12.0, - ), - ), - ), - Row( - children: [ - Icon( - Icons.favorite_border_outlined, - color: Colors.black54, - size: 16.0, - ), - Text( - '99', - style: TextStyle(color: Colors.black54, fontSize: 12.0), - ), - ], - ), - SizedBox( - width: 20.0, - ), - Row( - children: [ - Icon( - Icons.heart_broken_outlined, - color: Colors.black54, - size: 16.0, - ), - ], - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: EdgeInsets.symmetric(vertical: 5.0), - child: Text( - '${commentList[index]['desc']}', - style: TextStyle( - fontSize: 14.0, - ), - ), - ), - Row( - children: [ - Container( - margin: EdgeInsets.only(right: 15.0), - padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(20.0), - ), - child: Row(children: [ - Text( - '12回复', - style: TextStyle(fontSize: 12.0), - ), - Icon( - Icons.arrow_forward_ios, - size: 10.0, - ) - ]), - ), - Text( - '01-15 · 浙江', - style: TextStyle(color: Colors.grey, fontSize: 12.0), - ), - ], - ), - ], - ), - ); - }, - ), - ), - ), - GestureDetector( - child: Container( - margin: EdgeInsets.all(10.0), - height: 40.0, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(30.0), - ), - child: Row( - children: [ - SizedBox( - width: 15.0, - ), - Icon( - Icons.edit_note, - color: Colors.black54, - size: 16.0, - ), - SizedBox( - width: 5.0, - ), - Text( - '说点什么...', - style: TextStyle(color: Colors.black54, fontSize: 14.0), - ), - ], - ), - ), - onTap: () { - navigator?.push(FadeRoute(child: PopupReply( - onChanged: (value) { - debugPrint('评论内容: $value'); - }, - ))); - }, - ), - ], - ), - ); + return CommentBottomSheet( + videoId: videoId, + onCommentCountChanged: (newCount) { + // 更新对应视频的评论数量 + setState(() { + if (index < videoList.length) { + videoList[index]['commentsCounts'] = newCount; + } + }); + }); }, ); } @@ -488,36 +833,32 @@ class _AttentionModuleState extends State { ), // 会话列表 - Obx(() { - // 这里过滤掉有分组的会话 - final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); - if (filteredList.isEmpty) return SizedBox.shrink(); - return SizedBox( + if (chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).isNotEmpty) + SizedBox( height: 110, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: filteredList.length, - padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), + itemCount: chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).length, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), itemBuilder: (context, index) { + final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); return GestureDetector( - // 点击分享 onTap: () => handlCoverClick(filteredList[index].conversation), child: Container( width: 64, - margin: EdgeInsets.symmetric(horizontal: 8.0), + margin: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Image.asset('${chatController.chatList[index].faceUrl}', width: 48.0), NetworkOrAssetImage( imageUrl: filteredList[index].faceUrl, width: 48.0, height: 48.0, ), - SizedBox(height: 5), + const SizedBox(height: 5), Text( - '${filteredList[index].conversation.showName}', - style: TextStyle(fontSize: 12.0), + filteredList[index].conversation.showName ?? '', + style: const TextStyle(fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], @@ -526,8 +867,7 @@ class _AttentionModuleState extends State { ); }, ), - ); - }), + ), // 取消按钮 SafeArea( @@ -556,14 +896,82 @@ class _AttentionModuleState extends State { void handleShareClick(int index) { print("分享项 $index 被点击"); - final description = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '获取title失败'; - - if (index == 1) { - // 好友 - Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: 'https://baidu.com'); + final videoId = videoList[videoModuleController.videoPlayIndex.value]['id']; + final videoUrl = videoList[videoModuleController.videoPlayIndex.value]['url']; + final description = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '快来看看这个视频'; + var httpPrefix = 'http://43.143.227.203/adv'; + logger.i('分享链接地址----------------: $httpPrefix/goods-detail?id=$videoId'); + if (index == 0) { + // 分享好友 + Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: '$httpPrefix/video-detail?id=$videoId'); + } else if (index == 1) { + // 分享到朋友圈 + Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: '$httpPrefix/goods-detail?id=$videoId'); } else if (index == 2) { - // 朋友圈 - Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: 'https://baidu.com'); + // 复制链接到剪切板 + copyToClipboard(videoUrl); + } else if (index == 3) { + // 下载视频到本地 + _downloadVideoWithDio(videoUrl, description); + } + } + + // 复制链接到剪贴板 + void copyToClipboard(String text) async { + try { + await Clipboard.setData(ClipboardData(text: text)); + MyToast().tip( + title: '链接已复制到剪贴板', + position: 'center', + type: 'success', + ); + } catch (e) { + MyToast().tip( + title: '复制失败', + position: 'center', + type: 'success', + ); + } + } + + // 下载视频 + Future _downloadVideoWithDio(String videoUrl, String fileName) async { + try { + // 请求存储权限 + String? toastId; // 用于存储toast的ID,以便后续关闭 + var status = await Permissions.requestStoragePermission(); + if (!status) { + MyToast().tip( + title: '需要存储权限才能下载视频', + position: 'center', + type: 'success', + ); + return; + } + await DownloadManager.downloadFile( + url: videoUrl, + fileName: '$fileName.mp4', + onProgress: (progress) { + print("下载进度: $progress%"); + // 显示进度组件 + }, + onComplete: (filePath) { + MyToast().tip( + title: '下载完成', + position: 'center', + type: 'success', + ); + }, + onError: (error) { + MyToast().tip( + title: '下载失败: $error', + position: 'center', + type: 'error', + ); + }, + ); + } catch (e) { + print("下载视频失败: $e"); } } @@ -602,7 +1010,6 @@ class _AttentionModuleState extends State { @override Widget build(BuildContext context) { - // super.build(context); return Container( color: Colors.black, child: Column( @@ -612,15 +1019,12 @@ class _AttentionModuleState extends State { children: [ /// 垂直滚动模块 PageView.builder( - // 自定义滚动行为(支持桌面端滑动、去掉滚动条槽) scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), scrollDirection: Axis.vertical, controller: pageController, onPageChanged: (index) async { - // 更新当前播放视频索引 videoModuleController.updateVideoPlayIndex(index); setState(() { - // 重置slider参数 sliderValue = 0.0; sliderDraging = false; position = Duration.zero; @@ -628,18 +1032,17 @@ class _AttentionModuleState extends State { }); player.stop(); - // await player.open(Media(videoList[index]['src'])); await player.open(Media(videoList[index]['url'])); - // 如果滚动到列表末尾,且还有更多数据 if (index == videoList.length - 2 && !isLoadingMore) { - await fetchVideoList(); // 拉取更多 + await fetchVideoList(); } }, itemCount: videoList.length, itemBuilder: (context, index) { final videoWidth = videoList[index]['width'] ?? 1; - final videoHeight = videoList[index]['height'] ?? 1; // 防止除以0 + final videoHeight = videoList[index]['height'] ?? 1; final isHorizontal = videoWidth > videoHeight; + final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); return Stack( children: [ // 视频区域 @@ -651,17 +1054,14 @@ class _AttentionModuleState extends State { child: GestureDetector( child: Stack( children: [ - // 短视频插件 Visibility( visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, child: Video( controller: videoController, fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - // 无控制条 controls: NoVideoControls, ), ), - // 封面图,播放后透明度渐变为0(而不是直接隐藏) AnimatedOpacity( opacity: videoModuleController.videoPlayIndex.value == index && position > Duration(milliseconds: 100) ? 0.0 : 1.0, duration: Duration(milliseconds: 50), @@ -672,8 +1072,6 @@ class _AttentionModuleState extends State { height: double.infinity, ), ), - - // 播放/暂停按钮 StreamBuilder( stream: player.stream.playing, builder: (context, playing) { @@ -710,16 +1108,22 @@ class _AttentionModuleState extends State { child: Column( spacing: 15.0, children: [ - // 头像 Stack( children: [ SizedBox( height: 55.0, width: 48.0, child: GestureDetector( - onTap: () { + onTap: () async { player.pause(); - Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); + // 跳转到 Vloger 页面并等待返回结果 + final result = await Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); + if (result != null) { + // 处理返回的参数 + print('返回的数据: ${result['followStatus']}'); + player.play(); + videoList[index]['doIFollowVloger'] = result['followStatus']; + } }, child: UnconstrainedBox( alignment: Alignment.topCenter, @@ -732,7 +1136,7 @@ class _AttentionModuleState extends State { ), child: ClipOval( child: NetworkOrAssetImage( - imageUrl: videoList[index]['avatar'], + imageUrl: videoList[index]['commentUserFace'], ), ), ), @@ -756,16 +1160,23 @@ class _AttentionModuleState extends State { size: 14.0, ), ), - onTap: () { - setState(() { - videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; - }); + onTap: () async { + final vlogerId = videoList[index]['memberId']; + final doIFollowVloger = videoList[index]['doIFollowVloger']; + // 未关注点击才去关注 + if (doIFollowVloger == false) { + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + if (res.success) { + setState(() { + videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; + }); + } + } }, ), ), ], ), - // 点赞 GestureDetector( child: Column( children: [ @@ -782,12 +1193,15 @@ class _AttentionModuleState extends State { ], ), onTap: () { - setState(() { - videoList[index]['doILikeThisVlog'] = !videoList[index]['doILikeThisVlog']; - }); + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + if (videoList[index]['doILikeThisVlog'] == true) { + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + doUnLikeVideo(videoList[index]); + } else { + doLikeVideo(videoList[index]); + } }, ), - // 评论 GestureDetector( child: Column( children: [ @@ -807,21 +1221,6 @@ class _AttentionModuleState extends State { handleComment(index); }, ), - // Column( - // children: [ - // SvgPicture.asset( - // 'assets/images/svg/favor.svg', - // colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - // height: 40.0, - // width: 40.0, - // ), - // Text( - // '${videoList[index]['starNum']}', - // style: TextStyle(color: Colors.white, fontSize: 12.0), - // ), - // ], - // ), - // 转发 GestureDetector( child: Column( children: [ @@ -837,7 +1236,6 @@ class _AttentionModuleState extends State { handleShare(index); }, ), - //举报 GestureDetector( child: Column( children: [ @@ -849,14 +1247,21 @@ class _AttentionModuleState extends State { ), ], ), - onTap: () { - // 举报 - }, + onTap: ()async { + player.pause(); + // 跳转到举报页面并等待返回结果 + final result = await Get.toNamed( + '/report', + arguments: videoList[videoModuleController + .videoPlayIndex.value]); + if (result != null) { + player.play(); + }; + }, ), ], ), ), - // 底部信息区域 Positioned( bottom: 15.0, left: 10.0, @@ -865,13 +1270,12 @@ class _AttentionModuleState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '@${videoList[videoModuleController.videoPlayIndex.value]['nickname'] ?? '未知'}', + '@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}', style: const TextStyle(color: Colors.white, fontSize: 16.0), ), LayoutBuilder( builder: (context, constraints) { final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知'; - // 先用 TextPainter 判断是否超过 3 行 final span = TextSpan( text: text, style: const TextStyle(color: Colors.white, fontSize: 14.0), @@ -921,7 +1325,6 @@ class _AttentionModuleState extends State { ), ], )), - // mini播放进度条 Positioned( bottom: 0.0, left: 6.0, @@ -932,29 +1335,25 @@ class _AttentionModuleState extends State { child: SliderTheme( data: SliderThemeData( trackHeight: sliderDraging ? 6.0 : 2.0, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), // 调整滑块的大小 - // trackShape: RectangularSliderTrackShape(), // 使用矩形轨道形状 - overlayShape: RoundSliderOverlayShape(overlayRadius: 0), // 去掉Slider默认上下边距间隙 - inactiveTrackColor: Colors.white24, // 设置非活动进度条的颜色 - activeTrackColor: Colors.white, // 设置活动进度条的颜色 - thumbColor: Colors.white, // 设置滑块的颜色 - overlayColor: Colors.transparent, // 设置滑块覆盖层的颜色 + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), + overlayShape: RoundSliderOverlayShape(overlayRadius: 0), + inactiveTrackColor: Colors.white24, + activeTrackColor: Colors.white, + thumbColor: Colors.white, + overlayColor: Colors.transparent, ), child: Slider( value: sliderValue, onChanged: (value) async { - // debugPrint('当前视频播放时间$value'); setState(() { sliderValue = value; }); - // 跳转播放时间 await player.seek(duration * value.clamp(0.0, 1.0)); }, onChangeEnd: (value) async { setState(() { sliderDraging = false; }); - // 继续播放 if (!player.state.playing) { await player.play(); } @@ -969,7 +1368,6 @@ class _AttentionModuleState extends State { ), ), ), - // 播放位置指示器 Positioned( bottom: 100.0, left: 10.0, @@ -993,10 +1391,6 @@ class _AttentionModuleState extends State { ); }, ), - - /// 固定层 - // 红包广告,先不做 - // Ads(), ], ), ), diff --git a/lib/router/index.dart b/lib/router/index.dart index c137e63..893b032 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -16,6 +16,8 @@ import 'package:loopin/pages/my/setting.dart'; import 'package:loopin/pages/my/user_info.dart'; import 'package:loopin/pages/my/vloger.dart'; import 'package:loopin/pages/video/report.dart'; +import 'package:loopin/pages/search/index.dart'; +import 'package:loopin/pages/search/search-result.dart'; import '../layouts/index.dart'; /* 引入路由页面 */ @@ -38,6 +40,8 @@ final Map routes = { '/order/detail': const OrderDetail(), '/vloger': const Vloger(), '/report': const ReportPage(), + '/search': const SearchPage(), + '/search-result': const SearchResultPage(), //settins '/setting': const Setting(), '/userInfo': const UserInfo(),