2026/6/23 18:33:44
网站建设
项目流程
销售型网站建设,官网优化公司,h5免费制作平台易企秀官方,怎么开发自己的商城1. 这篇文章是跟练实践#xff0c;原文章链接如下#xff1a; https://blog.csdn.net/AHuiHatedebug/article/details/155170309?spm1001.2014.3001.5502 2. 项目背景介绍以及请求中用到的token获取#xff0c;请参考以下链接#xff1a; https://blog.csdn.net/AHuiHat…1. 这篇文章是跟练实践原文章链接如下https://blog.csdn.net/AHuiHatedebug/article/details/155170309?spm1001.2014.3001.55022. 项目背景介绍以及请求中用到的token获取请参考以下链接https://blog.csdn.net/AHuiHatedebug/article/details/155170169?spm1001.2014.3001.5502上篇文章实现了项目整体框架的搭建和UI界面的绘制本篇文章来介绍搜索相关网络接口的封装和搜索接口的实现。网络请求用的是dio这个三方库。在鸿蒙手机运行效果如下第一步创建 API 客户端基础框架1.1 创建 API 客户端文件创建lib/core/gitcode_api.dartimport package:dio/dio.dart; import package:flutter/foundation.dart; /// GitCode API 异常类 class GitCodeApiException implements Exception { const GitCodeApiException(this.message); final String message; override String toString() GitCodeApiException: $message; } /// GitCode API 客户端 class GitCodeApiClient { GitCodeApiClient({Dio? dio}) : _dio dio ?? Dio( BaseOptions( baseUrl: https://api.gitcode.com/api/v5, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), ), ); final Dio _dio; /// 构建请求头 MapString, String _buildHeaders(String? personalToken) { return { if (personalToken ! null personalToken.isNotEmpty) Authorization: Bearer $personalToken, }; } }代码说明GitCodeApiException自定义异常类用于包装 API 错误GitCodeApiClientAPI 客户端使用 Dio 发送 HTTP 请求baseUrlGitCode API v5 基础地址connectTimeout 和 receiveTimeout5 秒超时_buildHeaders构建请求头支持 Bearer Token 认证第二步创建数据模型2.1 用户搜索模型在lib/core/gitcode_api.dart文件末尾添加/// 搜索用户结果模型 class GitCodeSearchUser { const GitCodeSearchUser({ required this.login, required this.avatarUrl, this.name, this.htmlUrl, this.createdAt, }); final String login; // 登录名 final String avatarUrl; // 头像 URL final String? name; // 显示名称 final String? htmlUrl; // 主页链接 final String? createdAt; // 创建时间 /// 从 JSON 创建对象 factory GitCodeSearchUser.fromJson(MapString, dynamic json) { return GitCodeSearchUser( login: json[login] as String? ?? , avatarUrl: json[avatar_url] as String? ?? , name: json[name] as String?, htmlUrl: json[html_url] as String?, createdAt: json[created_at] as String?, ); } }代码说明login用户登录名必需avatarUrl头像地址必需name、htmlUrl、createdAt可选字段fromJson工厂构造函数从 JSON 创建对象2.2 仓库搜索模型继续在文件末尾添加/// 仓库模型 class GitCodeRepository { const GitCodeRepository({ required this.fullName, required this.webUrl, this.description, this.language, this.updatedAt, this.stars, this.forks, this.watchers, this.ownerLogin, this.isPrivate, this.id, this.projectId, }); final String fullName; // 完整名称owner/repo final String webUrl; // Web URL final String? description; // 描述 final String? language; // 主要语言 final String? updatedAt; // 更新时间 final int? stars; // Star 数 final int? forks; // Fork 数 final int? watchers; // Watch 数 final String? ownerLogin; // 所有者 final bool? isPrivate; // 是否私有 final int? id; // 仓库 ID final int? projectId; // 项目 ID factory GitCodeRepository.fromJson(MapString, dynamic json) { return GitCodeRepository( fullName: json[full_name] as String? ?? json[path_with_namespace] as String? ?? , webUrl: json[web_url] as String? ?? json[html_url] as String? ?? , description: json[description] as String?, language: json[language] as String?, updatedAt: json[updated_at] as String?, stars: _safeInt(json[stargazers_count] ?? json[star_count]), forks: _safeInt(json[forks_count] ?? json[forks]), watchers: _safeInt(json[watchers_count] ?? json[watchers]), ownerLogin: (json[owner] as MapString, dynamic?)?[login] as String?, isPrivate: _safeBool(json[private] ?? json[visibility] private), id: _safeInt(json[id]), projectId: _safeInt(json[project_id]), ); } } /// 安全地将 dynamic 转换为 int int? _safeInt(dynamic value) { if (value null) return null; if (value is int) return value; if (value is String) return int.tryParse(value); return null; } /// 安全地将 dynamic 转换为 bool bool? _safeBool(dynamic value) { if (value null) return null; if (value is bool) return value; if (value is int) return value ! 0; if (value is String) { return value 1 || value.toLowerCase() true; } return null; }第三步实现搜索用户 API3.1 添加搜索用户方法在GitCodeApiClient类中添加/// 调用 /search/users返回符合关键字的用户简要信息。 FutureListGitCodeSearchUser searchUsers({ required String keyword, required String personalToken, int perPage 10, int page 1, }) async { // 用户搜索同样需要去除前后空格避免无效查询。 final trimmed keyword.trim(); if (trimmed.isEmpty) { throw const GitCodeApiException(请输入搜索关键字); } final response await _dio.getListdynamic( /search/users, queryParameters: String, dynamic{ q: trimmed, access_token: personalToken, // clamp 可以阻止业务层传入不合法的分页参数避免后端报错。 per_page: perPage.clamp(1, 50), page: page.clamp(1, 100), }, options: Options( headers: _buildHeaders(personalToken), responseType: ResponseType.json, validateStatus: (status) status ! null status 500, ), ); final statusCode response.statusCode ?? 0; if (statusCode 401) { throw const GitCodeApiException(Token 无效或权限不足无法搜索用户); } if (statusCode ! 200 || response.data null) { throw GitCodeApiException(搜索用户失败 (HTTP $statusCode)); } return response.data! .whereTypeMapString, dynamic() .map(GitCodeSearchUser.fromJson) .toList(); }代码说明使用 _dio.get 发送 GET 请求路径/search/users参数access_token、q关键字、per_page、page使用 clamp 限制参数范围返回 ListGitCodeSearchUser完善的错误处理401未授权、404未找到、超时等第四步实现搜索仓库 API4.1 添加搜索仓库方法在GitCodeApiClient类中继续添加/// 调用 /search/repositories支持语言、排序等可选参数。 FutureListGitCodeRepository searchRepositories({ required String keyword, required String personalToken, String? language, String? sort, String? order, int perPage 10, int page 1, }) async { final trimmed keyword.trim(); if (trimmed.isEmpty) { throw const GitCodeApiException(请输入搜索关键字); } final queryParameters String, dynamic{ q: trimmed, access_token: personalToken, per_page: perPage.clamp(1, 50), page: page.clamp(1, 100), // 以下三个参数均为可选过滤项后端若为空会忽略。 if (language ! null language.isNotEmpty) language: language, if (sort ! null sort.isNotEmpty) sort: sort, if (order ! null order.isNotEmpty) order: order, }; final response await _dio.getListdynamic( /search/repositories, queryParameters: queryParameters, options: Options( headers: _buildHeaders(personalToken), responseType: ResponseType.json, validateStatus: (status) status ! null status 500, ), ); final statusCode response.statusCode ?? 0; if (statusCode 401) { throw const GitCodeApiException(Token 无效或权限不足无法搜索仓库); } if (statusCode ! 200 || response.data null) { throw GitCodeApiException(搜索仓库失败 (HTTP $statusCode)); } return response.data! .whereTypeMapString, dynamic() .map(GitCodeRepository.fromJson) .toList(); }第五步更新搜索页面实现真实搜索5.1 修改 search_page.dart打开lib/pages/main_navigation/search_page.dart添加导入import package:flutter/material.dart; import ../../core/gitcode_api.dart; // 添加这行在_SearchPageState类中添加变量class _SearchPageState extends StateSearchPage { final _client GitCodeApiClient(); // 添加 API 客户端 final _keywordController TextEditingController(); final _tokenController TextEditingController(); SearchMode _searchMode SearchMode.user; bool _tokenObscured true; // 添加搜索结果相关变量 bool _isSearching false; String? _errorMessage; ListGitCodeSearchUser _userResults []; ListGitCodeRepository _repoResults [];修改_performSearch方法/// 执行搜索 Futurevoid _performSearch() async { final keyword _keywordController.text.trim(); final token _tokenController.text.trim(); // 输入验证 if (keyword.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(请输入搜索关键字)), ); return; } if (token.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(请输入 Access Token)), ); return; } // 开始搜索 setState(() { _isSearching true; _errorMessage null; }); try { if (_searchMode SearchMode.user) { // 搜索用户 final users await _client.searchUsers( keyword: keyword, personalToken: token, perPage: 3, // 预览只显示 3 条 ); setState(() { _userResults users; _isSearching false; }); if (users.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(未找到用户)), ); } } else { // 搜索仓库 final repos await _client.searchRepositories( keyword: keyword, personalToken: token, perPage: 3, ); setState(() { _repoResults repos; _isSearching false; }); if (repos.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(未找到仓库)), ); } } } on GitCodeApiException catch (e) { setState(() { _errorMessage e.message; _isSearching false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } }5.2 添加搜索结果展示在build方法的Column中_buildUsageTips(theme)之前添加// 搜索结果 if (_isSearching) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) else if (_errorMessage ! null) _buildErrorView(theme) else if (_searchMode SearchMode.user _userResults.isNotEmpty) _buildUserResults(theme) else if (_searchMode SearchMode.repo _repoResults.isNotEmpty) _buildRepoResults(theme), const SizedBox(height: 16),添加结果展示方法/// 用户搜索结果 Widget _buildUserResults(ThemeData theme) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 搜索结果${_userResults.length}, style: theme.textTheme.titleMedium, ), TextButton( onPressed: () { // TODO: 跳转到完整列表页 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(即将跳转到用户列表)), ); }, child: const Text(查看全部), ), ], ), ), ...List.generate(_userResults.length, (index) { final user _userResults[index]; return ListTile( leading: CircleAvatar( backgroundImage: NetworkImage(user.avatarUrl), ), title: Text(user.name ?? user.login), subtitle: Text(${user.login}), trailing: const Icon(Icons.chevron_right), onTap: () { // TODO: 跳转到用户详情 }, ); }), ], ), ); } /// 仓库搜索结果 Widget _buildRepoResults(ThemeData theme) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 搜索结果${_repoResults.length}, style: theme.textTheme.titleMedium, ), TextButton( onPressed: () { // TODO: 跳转到完整列表页 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(即将跳转到仓库列表)), ); }, child: const Text(查看全部), ), ], ), ), ...List.generate(_repoResults.length, (index) { final repo _repoResults[index]; return ListTile( leading: Icon( repo.isPrivate true ? Icons.lock : Icons.folder, color: theme.colorScheme.primary, ), title: Text(repo.fullName), subtitle: repo.description ! null ? Text( repo.description!, maxLines: 1, overflow: TextOverflow.ellipsis, ) : null, trailing: const Icon(Icons.chevron_right), onTap: () { // TODO: 跳转到仓库详情 }, ); }), ], ), ); } /// 错误视图 Widget _buildErrorView(ThemeData theme) { return Card( child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ Icon( Icons.error_outline, size: 48, color: theme.colorScheme.error, ), const SizedBox(height: 16), Text( _errorMessage ?? 搜索失败, style: TextStyle(color: theme.colorScheme.error), textAlign: TextAlign.center, ), const SizedBox(height: 16), OutlinedButton( onPressed: _performSearch, child: const Text(重试), ), ], ), ), ); }第六步创建用户列表页面6.1 创建用户列表页文件创建lib/pages/user_list_page.dartimport package:flutter/material.dart; import package:pull_to_refresh/pull_to_refresh.dart; import ../core/gitcode_api.dart; class UserListPage extends StatefulWidget { const UserListPage({ super.key, required this.keyword, required this.token, }); final String keyword; final String token; override StateUserListPage createState() _UserListPageState(); } class _UserListPageState extends StateUserListPage { final _client GitCodeApiClient(); final _refreshController RefreshController(); ListGitCodeSearchUser _users []; int _currentPage 1; final int _perPage 20; bool _hasMore true; bool _isLoading false; String? _errorMessage; override void initState() { super.initState(); _loadUsers(refresh: true); } override void dispose() { _refreshController.dispose(); super.dispose(); } /// 加载用户数据 Futurevoid _loadUsers({bool refresh false}) async { if (_isLoading) return; if (refresh) { _currentPage 1; _hasMore true; _users.clear(); } if (!_hasMore) { _refreshController.loadNoData(); return; } setState(() { _isLoading true; _errorMessage null; }); try { final users await _client.searchUsers( keyword: widget.keyword, personalToken: widget.token, perPage: _perPage, page: _currentPage, ); setState(() { if (refresh) { _users users; } else { _users.addAll(users); } _hasMore users.length _perPage; _currentPage; _isLoading false; }); if (refresh) { _refreshController.refreshCompleted(); } else { _hasMore ? _refreshController.loadComplete() : _refreshController.loadNoData(); } } on GitCodeApiException catch (e) { setState(() { _errorMessage e.message; _isLoading false; }); refresh ? _refreshController.refreshFailed() : _refreshController.loadFailed(); } } override Widget build(BuildContext context) { final theme Theme.of(context); return Scaffold( appBar: AppBar( title: Text(用户搜索: ${widget.keyword}), ), body: _buildBody(theme), ); } Widget _buildBody(ThemeData theme) { // 加载中首次 if (_isLoading _users.isEmpty _errorMessage null) { return const Center(child: CircularProgressIndicator()); } // 错误状态 if (_errorMessage ! null _users.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[300]), const SizedBox(height: 16), Text(_errorMessage!, style: TextStyle(color: Colors.red[700])), const SizedBox(height: 16), ElevatedButton( onPressed: () _loadUsers(refresh: true), child: const Text(重试), ), ], ), ); } // 空状态 if (_users.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.person_off, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text(未找到用户, style: TextStyle(color: Colors.grey[600])), ], ), ); } // 列表 return SmartRefresher( controller: _refreshController, enablePullDown: true, enablePullUp: _hasMore, header: const ClassicHeader( refreshingText: 刷新中..., completeText: 刷新完成, idleText: 下拉刷新, releaseText: 释放刷新, ), footer: const ClassicFooter( loadingText: 加载中..., noDataText: 没有更多数据了, idleText: 上拉加载更多, canLoadingText: 释放加载, ), onRefresh: () _loadUsers(refresh: true), onLoading: () _loadUsers(refresh: false), child: ListView.builder( itemCount: _users.length, itemBuilder: (context, index) { final user _users[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: ListTile( leading: CircleAvatar( backgroundImage: NetworkImage(user.avatarUrl), ), title: Text(user.name ?? user.login), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(${user.login}), if (user.createdAt ! null) Text( 加入于 ${user.createdAt!.substring(0, 10)}, style: theme.textTheme.bodySmall, ), ], ), trailing: const Icon(Icons.chevron_right), onTap: () { // TODO: 跳转到用户详情 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(点击了 ${user.login})), ); }, ), ); }, ), ); } }代码说明使用SmartRefresher实现下拉刷新和上拉加载_currentPage和_hasMore管理分页状态完整的状态处理加载中、错误、空数据、成功RefreshController需要在dispose中释放6.2 更新搜索页面跳转修改search_page.dart中的_buildUserResults方法TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) UserListPage( keyword: _keywordController.text.trim(), token: _tokenController.text.trim(), ), ), ); }, child: const Text(查看全部), ),同时添加导入import ../user_list_page.dart; // 在文件顶部添加第七步创建仓库列表页面7.1 创建仓库列表页文件创建lib/pages/repository_list_page.dartimport package:flutter/material.dart; import package:pull_to_refresh/pull_to_refresh.dart; import ../core/gitcode_api.dart; class RepositoryListPage extends StatefulWidget { const RepositoryListPage({ super.key, required this.keyword, required this.token, }); final String keyword; final String token; override StateRepositoryListPage createState() _RepositoryListPageState(); } class _RepositoryListPageState extends StateRepositoryListPage { final _client GitCodeApiClient(); final _refreshController RefreshController(); ListGitCodeRepository _repositories []; int _currentPage 1; final int _perPage 20; bool _hasMore true; bool _isLoading false; String? _errorMessage; override void initState() { super.initState(); _loadRepositories(refresh: true); } override void dispose() { _refreshController.dispose(); super.dispose(); } Futurevoid _loadRepositories({bool refresh false}) async { if (_isLoading) return; if (refresh) { _currentPage 1; _hasMore true; _repositories.clear(); } if (!_hasMore) { _refreshController.loadNoData(); return; } setState(() { _isLoading true; _errorMessage null; }); try { final repos await _client.searchRepositories( keyword: widget.keyword, personalToken: widget.token, perPage: _perPage, page: _currentPage, ); setState(() { if (refresh) { _repositories repos; } else { _repositories.addAll(repos); } _hasMore repos.length _perPage; _currentPage; _isLoading false; }); if (refresh) { _refreshController.refreshCompleted(); } else { _hasMore ? _refreshController.loadComplete() : _refreshController.loadNoData(); } } on GitCodeApiException catch (e) { setState(() { _errorMessage e.message; _isLoading false; }); refresh ? _refreshController.refreshFailed() : _refreshController.loadFailed(); } } override Widget build(BuildContext context) { final theme Theme.of(context); return Scaffold( appBar: AppBar( title: Text(仓库搜索: ${widget.keyword}), ), body: _buildBody(theme), ); } Widget _buildBody(ThemeData theme) { if (_isLoading _repositories.isEmpty _errorMessage null) { return const Center(child: CircularProgressIndicator()); } if (_errorMessage ! null _repositories.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[300]), const SizedBox(height: 16), Text(_errorMessage!, style: TextStyle(color: Colors.red[700])), const SizedBox(height: 16), ElevatedButton( onPressed: () _loadRepositories(refresh: true), child: const Text(重试), ), ], ), ); } if (_repositories.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.folder_off, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text(未找到仓库, style: TextStyle(color: Colors.grey[600])), ], ), ); } return SmartRefresher( controller: _refreshController, enablePullDown: true, enablePullUp: _hasMore, header: const ClassicHeader( refreshingText: 刷新中..., completeText: 刷新完成, idleText: 下拉刷新, releaseText: 释放刷新, ), footer: const ClassicFooter( loadingText: 加载中..., noDataText: 没有更多数据了, idleText: 上拉加载更多, canLoadingText: 释放加载, ), onRefresh: () _loadRepositories(refresh: true), onLoading: () _loadRepositories(refresh: false), child: ListView.builder( itemCount: _repositories.length, itemBuilder: (context, index) { final repo _repositories[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Icon( repo.isPrivate true ? Icons.lock : Icons.folder, color: theme.colorScheme.primary, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( repo.fullName, style: theme.textTheme.titleMedium, ), ), ], ), // 描述 if (repo.description ! null) ...[ const SizedBox(height: 8), Text( repo.description!, style: theme.textTheme.bodyMedium, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], // 统计信息 const SizedBox(height: 12), Row( children: [ if (repo.language ! null) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( repo.language!, style: const TextStyle( fontSize: 12, color: Colors.blue, ), ), ), const SizedBox(width: 12), ], if (repo.stars ! null) ...[ const Icon(Icons.star, size: 16, color: Colors.amber), const SizedBox(width: 4), Text(${repo.stars}), const SizedBox(width: 12), ], if (repo.forks ! null) ...[ const Icon(Icons.call_split, size: 16), const SizedBox(width: 4), Text(${repo.forks}), ], ], ), ], ), ), ); }, ), ); } }7.2 更新搜索页面跳转修改search_page.dart中的_buildRepoResults方法TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) RepositoryListPage( keyword: _keywordController.text.trim(), token: _tokenController.text.trim(), ), ), ); }, child: const Text(查看全部), ),同时添加导入import ../repository_list_page.dart; // 在文件顶部添加第八步测试搜索功能8.1 运行应用flutter run8.2 测试步骤1. 获取 Access Token访问 https://gitcode.com登录后进入设置 → 访问令牌创建新令牌并复制2. 测试用户搜索进入搜索页面选择用户模式输入关键字如flutter输入 Access Token点击开始搜索验证显示搜索结果点击查看全部进入列表页测试下拉刷新滚动到底部测试上拉加载3. 测试仓库搜索切换到仓库模式重复上述步骤项目结构更新lib/ ├── core/ │ ├── app_config.dart │ └── gitcode_api.dart ← 新增 ├── pages/ │ ├── main_navigation/ │ │ ├── intro_page.dart │ │ ├── search_page.dart ← 更新 │ │ └── profile_page.dart │ ├── user_list_page.dart ← 新增 │ └── repository_list_page.dart ← 新增 └── main.dart