1、搜索页、搜索结果页
This commit is contained in:
parent
1a959e7c3b
commit
0e63d49814
215
lib/pages/search/index.dart
Normal file
215
lib/pages/search/index.dart
Normal file
@ -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<SearchPage> createState() => _SearchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageState extends State<SearchPage> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final GetStorage _storage = GetStorage();
|
||||||
|
List<dynamic> _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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
622
lib/pages/search/search-result.dart
Normal file
622
lib/pages/search/search-result.dart
Normal file
@ -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<SearchResultPage> createState() => _SearchResultPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchResultPageState extends State<SearchResultPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
String _searchQuery = Get.parameters?['searchWords'] ?? '';
|
||||||
|
int _initialTabIndex = 0;
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final FocusNode _searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
// 三个tab的数据
|
||||||
|
List<dynamic> _videoResults = [];
|
||||||
|
List<dynamic> _productResults = [];
|
||||||
|
List<dynamic> _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<void> _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<void> _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<String, dynamic>;
|
||||||
|
return _buildVideoItem(video);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoItem(Map<String, dynamic> 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<String, dynamic>;
|
||||||
|
return _buildProductItem(product);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProductItem(Map<String, dynamic> 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<String, dynamic>;
|
||||||
|
return _buildUserItem(user);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserItem(Map<String, dynamic> 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -128,7 +128,9 @@ class _VideoPageState extends State<VideoPage> with SingleTickerProviderStateMix
|
|||||||
Icons.search_rounded,
|
Icons.search_rounded,
|
||||||
color: tabColor(),
|
color: tabColor(),
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
Get.toNamed('/search');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,8 @@ import 'package:loopin/pages/my/setting.dart';
|
|||||||
import 'package:loopin/pages/my/user_info.dart';
|
import 'package:loopin/pages/my/user_info.dart';
|
||||||
import 'package:loopin/pages/my/vloger.dart';
|
import 'package:loopin/pages/my/vloger.dart';
|
||||||
import 'package:loopin/pages/video/report.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';
|
import '../layouts/index.dart';
|
||||||
/* 引入路由页面 */
|
/* 引入路由页面 */
|
||||||
@ -38,6 +40,8 @@ final Map<String, Widget> routes = {
|
|||||||
'/order/detail': const OrderDetail(),
|
'/order/detail': const OrderDetail(),
|
||||||
'/vloger': const Vloger(),
|
'/vloger': const Vloger(),
|
||||||
'/report': const ReportPage(),
|
'/report': const ReportPage(),
|
||||||
|
'/search': const SearchPage(),
|
||||||
|
'/search-result': const SearchResultPage(),
|
||||||
//settins
|
//settins
|
||||||
'/setting': const Setting(),
|
'/setting': const Setting(),
|
||||||
'/userInfo': const UserInfo(),
|
'/userInfo': const UserInfo(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user