【学习打卡】day03基于flutter实现鸿蒙应用开发之gitcode口袋工具箱完善版本
首先,我们将代码拆分为模块化结构,便于维护和扩展:plaintext二、逐步实现各模块1. 常量定义()作用:集中管理 API 地址、主题色等常量,便于统一修改。dart2. 毛玻璃二次元主题()作用:定义应用的整体风格,包括颜色、字体、组件样式,重点实现毛玻璃效果。dart3. 毛玻璃卡片组件()作用:封装毛玻璃效果,供各页面复用。dart4. 服务层()作用:处理 API 请求,封装网络逻辑。
·
一、项目结构规划
首先,我们将代码拆分为模块化结构,便于维护和扩展下面是文件结构:
plaintext
lib/
├── main.dart # 应用入口
├── constants/ # 常量定义
│ └── app_constants.dart # API地址、主题色等
├── themes/ # 主题配置
│ └── app_theme.dart # 毛玻璃二次元主题
├── services/ # 服务层
│ └── gitcode_service.dart # GitCode API请求
├── models/ # 数据模型(可选,当前使用Map)
├── screens/ # 页面组件
│ ├── login_screen.dart # 登录页
│ ├── main_screen.dart # 主页面(底部导航)
│ ├── home_screen.dart # 首页动态
│ ├── repo_screen.dart # 仓库页
│ └── profile_screen.dart # 个人中心
└── widgets/ # 自定义组件
├── glass_card.dart # 毛玻璃卡片组件
└── activity_card.dart # 动态卡片组件
二、逐步实现各模块
1. 常量定义(constants/app_constants.dart)
作用:集中管理 API 地址、主题色等常量,便于统一修改。
dart
// lib/constants/app_constants.dart
class AppConstants {
static const String apiBaseUrl = 'https://api.gitcode.com/api/v5';
// 单个令牌存储键
static const String tokenStorageKey = 'gitcode_token';
// 🔴 新增:历史令牌存储键
static const String tokenHistoryKey = 'gitcode_token_history';
// 🔴 新增:历史记录最大数量
static const int maxHistoryCount = 5;
}
2. 毛玻璃二次元主题(themes/app_theme.dart)
作用:定义应用的整体风格,包括颜色、字体、组件样式,重点实现毛玻璃效果。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
/// 毛玻璃二次元主题配置
class AppTheme {
/// 字体大小:超大
static const double fontSizeXXLarge = 24;
/// 字体大小:大
static const double fontSizeXLarge = 18;
/// 字体大小:中
static const double fontSizeLarge = 16;
/// 字体大小:中
static const double fontSizeMedium = 14;
/// 字体大小:小
static const double fontSizeSmall = 12;
/// 主色调(二次元粉紫色)
static const Color primaryColor = Color(0xFF8B5CF6);
/// 次要色调(天空蓝)
static const Color secondaryColor = Color(0xFF38BDF8);
/// 强调色(樱花粉)
static const Color accentColor = Color(0xFFEC4899);
/// 背景色(浅紫灰)
static const Color backgroundColor = Color(0xFFF5F3FF);
/// 卡片背景色(半透明白)
static const Color cardBackgroundColor = Color.fromARGB(200, 255, 255, 255);
/// 文字主色
static const Color primaryTextColor = Color(0xFF1E1B4B);
/// 文字次要色
static const Color secondaryTextColor = Color(0xFF64748B);
/// 边框颜色
static const Color borderColor = Color.fromARGB(50, 139, 92, 246);
/// 阴影效果
static const BoxShadow cardShadow = BoxShadow(
color: Color.fromRGBO(139, 92, 246, 0.1), // 主题色阴影
blurRadius: 12, // 模糊半径
offset: Offset(0, 4), // 偏移量
);
/// 圆角大小
static const BorderRadius cardBorderRadius = BorderRadius.all(Radius.circular(16));
/// 构建Material主题
static ThemeData get themeData => ThemeData(
// 主色调
primaryColor: primaryColor,
// 画布背景色
scaffoldBackgroundColor: backgroundColor,
// 卡片主题
cardTheme: CardTheme(
color: cardBackgroundColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: cardBorderRadius),
shadowColor: cardShadow.color,
),
// AppBar主题
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent, // 透明背景(配合毛玻璃)
foregroundColor: primaryTextColor, // 文字颜色
elevation: 0, // 无阴影
centerTitle: true, // 标题居中
),
// 底部导航栏主题
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: cardBackgroundColor, // 半透明背景
selectedItemColor: primaryColor, // 选中颜色
unselectedItemColor: secondaryTextColor, // 未选中颜色
elevation: 8, // 阴影
type: BottomNavigationBarType.fixed, // 固定类型
),
// 文字主题
textTheme: const TextTheme(
bodyLarge: TextStyle(color: primaryTextColor),
bodyMedium: TextStyle(color: secondaryTextColor),
titleLarge: TextStyle(
fontWeight: FontWeight.bold,
color: primaryTextColor,
),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor, // 按钮背景色
foregroundColor: Colors.white, // 按钮文字色
shape: RoundedRectangleBorder(borderRadius: cardBorderRadius), // 圆角
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), // 内边距
),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
filled: true, // 填充背景
fillColor: cardBackgroundColor, // 填充色
border: OutlineInputBorder(
borderRadius: cardBorderRadius, // 圆角
borderSide: BorderSide(color: borderColor), // 边框色
),
enabledBorder: OutlineInputBorder(
borderRadius: cardBorderRadius,
borderSide: BorderSide(color: borderColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: cardBorderRadius,
borderSide: BorderSide(color: primaryColor, width: 2), // 聚焦边框色
),
),
);
}
3. 毛玻璃卡片组件(widgets/glass_card.dart)
作用:封装毛玻璃效果,供各页面复用。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import 'dart:ui'; // 用于ImageFilter(毛玻璃)
import '../themes/app_theme.dart';
/// 毛玻璃卡片组件
class GlassCard extends StatelessWidget {
/// 卡片子组件
final Widget child;
/// 内边距
final EdgeInsets padding;
/// 外边距
final EdgeInsets margin;
const GlassCard({
Key? key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.margin = const EdgeInsets.only(bottom: 16),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: margin,
// 毛玻璃效果核心:BackdropFilter + ImageFilter.blur
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), // 模糊程度
child: Container(
padding: padding,
decoration: BoxDecoration(
color: AppTheme.cardBackgroundColor, // 半透明背景
borderRadius: AppTheme.cardBorderRadius, // 圆角
boxShadow: [AppTheme.cardShadow], // 阴影
border: Border.all(
color: Colors.white.withOpacity(0.2), // 白色描边(增强玻璃感)
width: 1,
),
),
child: child,
),
),
);
}
}
4. 服务层(services/gitcode_service.dart)
作用:处理 API 请求,封装网络逻辑。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:dio/dio.dart';
import '../constants/app_constants.dart';
/// GitCode API服务类
class GitCodeService {
// 单例模式:确保全局只有一个实例
static final GitCodeService _instance = GitCodeService._internal();
factory GitCodeService() => _instance;
late Dio dio; // Dio实例,用于网络请求
// 内部构造函数
GitCodeService._internal() {
// 初始化Dio
dio = Dio(
BaseOptions(
baseUrl: AppConstants.apiBaseUrl, // API基础地址
connectTimeout: const Duration(seconds: 15), // 连接超时
receiveTimeout: const Duration(seconds: 15), // 接收超时
responseType: ResponseType.json, // 响应类型
contentType: 'application/json; charset=utf-8', // 请求类型
headers: {
'Accept': 'application/json', // 接受JSON格式
},
),
);
// 添加拦截器(日志、错误处理)
dio.interceptors.add(
InterceptorsWrapper(
// 请求拦截
onRequest: (options, handler) {
// 如果有令牌,添加到请求头
String? token = _accessToken;
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
print('🚀 请求: ${options.method} ${options.uri}');
handler.next(options); // 继续请求
},
// 响应拦截
onResponse: (response, handler) {
print('✅ 响应: ${response.statusCode} ${response.requestOptions.uri}');
handler.next(response); // 继续处理响应
},
// 错误拦截
onError: (DioException e, handler) {
print('❌ 错误: ${e.message}');
print('❌ 响应状态: ${e.response?.statusCode}');
print('❌ 响应体: ${e.response?.data}');
// 统一错误信息
String errorMsg = '网络请求失败';
if (e.response?.statusCode == 404) {
errorMsg = 'API端点不存在';
} else if (e.response?.statusCode == 401) {
errorMsg = '无效的访问令牌';
} else if (e.type == DioExceptionType.connectionTimeout) {
errorMsg = '网络超时,请检查网络连接';
}
// 包装错误
e = DioException(
requestOptions: e.requestOptions,
type: e.type,
error: errorMsg,
response: e.response,
);
handler.next(e); // 继续处理错误
},
),
);
}
// 令牌存储(全局变量,实际项目建议使用本地存储)
static String? _accessToken;
/// 设置访问令牌
void setToken(String token) {
_accessToken = token;
}
/// 清除访问令牌
void clearToken() {
_accessToken = null;
}
/// 获取当前用户信息
Future<Map<String, dynamic>> fetchUserInfo() async {
try {
final response = await dio.get('/user'); // 请求用户信息
return response.data as Map<String, dynamic>; // 返回数据
} on DioException catch (e) {
throw Exception(e.error ?? '获取用户信息失败'); // 抛出异常
}
}
/// 获取用户仓库列表
Future<List<dynamic>> fetchMyRepositories() async {
try {
final response = await dio.get(
'/user/repos',
queryParameters: {
'visibility': 'all', // 所有可见性
'affiliation': 'owner', // 仅自己的仓库
'page': 1, // 页码
'per_page': 20, // 每页数量
},
);
return response.data as List<dynamic>; // 返回仓库列表
} on DioException catch (e) {
throw Exception(e.error ?? '获取我的仓库失败'); // 抛出异常
}
}
/// 获取动态列表
Future<List<dynamic>> fetchActivities() async {
try {
final response = await dio.get(
'/events',
queryParameters: {'page': 1, 'per_page': 20}, // 分页参数
);
return response.data as List<dynamic>; // 返回动态列表
} catch (e) {
print('⚠️ 动态API调用失败,返回模拟数据: $e');
// 返回模拟数据(防止API不可用导致崩溃)
return [
{
"id": 1,
"type": "PushEvent",
"actor": {"login": "user1", "avatar_url": "https://placehold.co/40x40/8B5CF6/ffffff?text=U1"},
"repo": {"name": "user1/repo1"},
"created_at": DateTime.now().subtract(const Duration(hours: 1)).toIso8601String()
},
{
"id": 2,
"type": "WatchEvent",
"actor": {"login": "user2", "avatar_url": "https://placehold.co/40x40/38BDF8/ffffff?text=U2"},
"repo": {"name": "user2/repo2"},
"created_at": DateTime.now().subtract(const Duration(hours: 2)).toIso8601String()
},
];
}
}
/// 获取热门仓库
Future<List<dynamic>> fetchHotRepositories() async {
try {
final response = await dio.get(
'/search/repositories',
queryParameters: {
'q': 'stars:>100', // 搜索条件:星数>100
'sort': 'stars', // 按星数排序
'order': 'desc', // 降序
'page': 1, // 页码
'per_page': 10, // 每页数量
},
);
final data = response.data as Map<String, dynamic>;
return data['items'] as List<dynamic>; // 返回热门仓库列表
} catch (e) {
print('⚠️ 热门仓库API调用失败,返回模拟数据: $e');
// 返回模拟数据
return [
{
"id": 101,
"name": "flutter-kit",
"full_name": "google/flutter-kit",
"description": "Flutter 开发工具包",
"language": "Dart",
"stargazers_count": 5000,
"forks_count": 1200,
"html_url": "https://gitcode.com/google/flutter-kit"
},
];
}
}
}
5. 登录页面(screens/login_screen.dart)
作用:处理用户登录逻辑,获取访问令牌。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import '../services/gitcode_service.dart';
import '../themes/app_theme.dart';
import '../widgets/glass_card.dart';
import './main_screen.dart';
/// 登录页面
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
/// 令牌输入控制器
final TextEditingController _tokenController = TextEditingController();
/// 加载状态
bool _isLoading = false;
/// 错误信息
String? _errorMessage;
/// 登录处理函数
void _handleLogin() async {
setState(() {
_isLoading = true; // 显示加载中
_errorMessage = null; // 清空错误信息
});
final token = _tokenController.text.trim(); // 获取输入的令牌
if (token.isEmpty) {
setState(() {
_isLoading = false;
_errorMessage = '请输入访问令牌'; // 空令牌提示
});
return;
}
try {
// 初始化服务并设置令牌
final service = GitCodeService();
service.setToken(token);
// 验证令牌是否有效(调用用户信息API)
await service.fetchUserInfo();
// 登录成功,跳转到主页面
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainScreen()),
);
}
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString(); // 显示错误信息
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 背景渐变(增强二次元风格)
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF5F3FF), // 浅紫灰
Color(0xFFE0E7FF), // 浅蓝紫
],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// Logo区域
GlassCard(
margin: const EdgeInsets.only(bottom: 32),
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Logo图标
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [AppTheme.cardShadow],
),
child: const Icon(
Icons.code,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 16),
// 应用标题
const Text(
'GitCode 客户端',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
),
),
const SizedBox(height: 8),
// 副标题
const Text(
'使用访问令牌登录',
style: TextStyle(
fontSize: 16,
color: AppTheme.secondaryTextColor,
),
),
],
),
),
// 登录表单
GlassCard(
child: Column(
children: [
// 令牌输入框
TextField(
controller: _tokenController,
obscureText: true, // 密码模式(隐藏令牌)
decoration: InputDecoration(
labelText: '访问令牌',
hintText: 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
hintStyle: const TextStyle(color: AppTheme.secondaryTextColor),
errorText: _errorMessage, // 错误提示
prefixIcon: const Icon(Icons.security, color: AppTheme.primaryColor),
suffixIcon: IconButton(
icon: const Icon(Icons.clear, color: AppTheme.secondaryTextColor),
onPressed: () => _tokenController.clear(), // 清空输入
),
),
maxLines: 1,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _handleLogin(), // 回车登录
),
const SizedBox(height: 16),
// 帮助链接
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// 打开获取令牌的帮助页面(预留)
print('打开获取令牌帮助');
},
child: const Text(
'如何获取令牌?',
style: TextStyle(color: AppTheme.primaryColor),
),
),
),
const SizedBox(height: 24),
// 登录按钮
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _handleLogin, // 加载中禁用
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
)
: const Icon(Icons.login),
label: const Text(
'登录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
],
),
),
),
),
),
);
}
}
6. 主页面(screens/main_screen.dart)
作用:底部导航栏,切换不同页面。
dart
// SPDX-License-Identifier: Apache-2.0
import 'dart:ui'; // 用于 ImageFilter(毛玻璃效果)
import 'package:flutter/material.dart';
import './home_screen.dart';
import './repo_screen.dart';
import './profile_screen.dart';
import '../themes/app_theme.dart';
/// 主页面(底部导航)
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
/// 当前选中的页面索引
int _selectedIndex = 0;
/// 页面列表
static const List<Widget> _widgetOptions = <Widget>[
HomeScreen(), // 首页动态
RepoScreen(), // 仓库页
ProfileScreen(), // 个人中心
];
/// 导航栏点击事件
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index; // 更新选中索引
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 背景渐变
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF5F3FF),
Color(0xFFE0E7FF),
],
),
),
child: _widgetOptions.elementAt(_selectedIndex), // 显示当前选中页面
),
// 底部导航栏
bottomNavigationBar: Container(
// 毛玻璃效果
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.code_outlined),
activeIcon: Icon(Icons.code),
label: '仓库',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outlined),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
),
),
);
}
}
7. 首页动态(screens/home_screen.dart)
作用:显示 GitCode 动态列表。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import '../services/gitcode_service.dart';
import '../themes/app_theme.dart';
import '../widgets/glass_card.dart';
/// 首页动态页面
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
/// GitCode服务实例
final GitCodeService _service = GitCodeService();
/// 动态列表数据
List<dynamic> _activities = [];
/// 加载状态
bool _isLoading = true;
/// 错误信息
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchActivities(); // 初始化时加载动态
}
/// 获取动态列表
Future<void> _fetchActivities() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final activities = await _service.fetchActivities();
setState(() {
_activities = activities;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 透明AppBar(配合背景渐变)
appBar: AppBar(
title: const Text('首页动态'),
backgroundColor: Colors.transparent,
),
body: RefreshIndicator(
onRefresh: _fetchActivities, // 下拉刷新
color: AppTheme.primaryColor,
backgroundColor: AppTheme.cardBackgroundColor,
child: _isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppTheme.primaryColor),
SizedBox(height: 16),
Text('加载中...'),
],
),
)
: _errorMessage != null
? Center(
child: GlassCard(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.secondaryTextColor),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchActivities,
child: const Text('重试'),
),
],
),
),
)
: _activities.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: AppTheme.secondaryTextColor,
),
SizedBox(height: 16),
Text('暂无动态'),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _activities.length,
itemBuilder: (context, index) {
final activity = _activities[index];
return _buildActivityCard(activity); // 构建动态卡片
},
),
),
);
}
/// 构建动态卡片
Widget _buildActivityCard(Map<String, dynamic> activity) {
return GlassCard(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头像
CircleAvatar(
radius: 28,
backgroundImage: NetworkImage(activity['actor']?['avatar_url'] ?? ''),
backgroundColor: AppTheme.secondaryColor.withOpacity(0.1),
),
const SizedBox(width: 16),
// 内容区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 用户名和操作
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${activity['actor']?['login'] ?? '未知用户'} ',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
fontSize: AppTheme.fontSizeMedium,
),
),
TextSpan(
text: _getActivityMessage(activity), // 获取操作描述
style: TextStyle(
color: AppTheme.secondaryTextColor,
fontSize: AppTheme.fontSizeMedium,
),
),
],
),
),
// 仓库名
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
'仓库: ${activity['repo']?['name'] ?? '未知仓库'}',
style: TextStyle(
color: AppTheme.secondaryTextColor,
fontSize: AppTheme.fontSizeSmall,
),
),
),
// 时间
Text(
_formatTime(activity['created_at']), // 格式化时间
style: TextStyle(
color: AppTheme.secondaryTextColor,
fontSize: AppTheme.fontSizeSmall,
),
),
],
),
),
// 操作图标
Icon(
_getActivityIcon(activity), // 获取操作图标
color: AppTheme.secondaryTextColor,
size: 20,
),
],
),
);
}
/// 获取操作描述
String _getActivityMessage(Map<String, dynamic> activity) {
final type = activity['type'] ?? 'unknown';
switch (type) {
case 'PushEvent':
return '提交了代码';
case 'WatchEvent':
return '点赞了仓库';
case 'FollowEvent':
return '关注了用户';
case 'IssuesEvent':
return '创建了Issue';
default:
return '进行了操作';
}
}
/// 获取操作图标
IconData _getActivityIcon(Map<String, dynamic> activity) {
final type = activity['type'] ?? 'unknown';
switch (type) {
case 'PushEvent':
return Icons.code;
case 'WatchEvent':
return Icons.star_outline;
case 'FollowEvent':
return Icons.person_add_outlined;
case 'IssuesEvent':
return Icons.assignment_outlined;
default:
return Icons.notification_add_outlined;
}
}
/// 格式化时间
String _formatTime(String? timeStr) {
if (timeStr == null) return '';
final date = DateTime.parse(timeStr);
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inMinutes < 1) {
return '刚刚';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes}分钟前';
} else if (difference.inHours < 24) {
return '${difference.inHours}小时前';
} else if (difference.inDays < 30) {
return '${difference.inDays}天前';
} else {
return '${date.month}/${date.day}';
}
}
}
8. 仓库页面(screens/repo_screen.dart)
作用:显示热门仓库和我的仓库。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import '../services/gitcode_service.dart';
import '../themes/app_theme.dart';
import '../widgets/glass_card.dart';
/// 仓库页面
class RepoScreen extends StatefulWidget {
const RepoScreen({Key? key}) : super(key: key);
@override
State<RepoScreen> createState() => _RepoScreenState();
}
class _RepoScreenState extends State<RepoScreen> {
/// GitCode服务实例
final GitCodeService _service = GitCodeService();
/// 是否显示我的仓库(false: 热门仓库,true: 我的仓库)
bool _showMyRepos = false;
/// 仓库列表数据
List<dynamic> _repos = [];
/// 加载状态
bool _isLoading = true;
/// 错误信息
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchRepos(); // 初始化时加载仓库
}
/// 获取仓库列表
Future<void> _fetchRepos() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final repos = _showMyRepos
? await _service.fetchMyRepositories() // 我的仓库
: await _service.fetchHotRepositories(); // 热门仓库
setState(() {
_repos = repos;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('仓库'),
backgroundColor: Colors.transparent,
),
body: RefreshIndicator(
onRefresh: _fetchRepos, // 下拉刷新
color: AppTheme.primaryColor,
backgroundColor: AppTheme.cardBackgroundColor,
child: Column(
children: [
// 切换标签(热门/我的)
GlassCard(
margin: const EdgeInsets.all(16),
padding: EdgeInsets.zero,
child: ToggleButtons(
isSelected: [!_showMyRepos, _showMyRepos],
onPressed: (index) {
setState(() {
_showMyRepos = index == 1; // 切换标签
_fetchRepos(); // 重新加载数据
});
},
borderRadius: AppTheme.cardBorderRadius,
selectedBorderColor: AppTheme.primaryColor,
selectedColor: Colors.white,
fillColor: AppTheme.primaryColor,
color: AppTheme.secondaryTextColor,
borderColor: AppTheme.borderColor,
constraints: const BoxConstraints(
minHeight: 44.0,
minWidth: 140.0,
),
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(
'热门仓库',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(
'我的仓库',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
// 仓库列表
Expanded(
child: _isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppTheme.primaryColor),
SizedBox(height: 16),
Text('加载中...'),
],
),
)
: _errorMessage != null
? Center(
child: GlassCard(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.secondaryTextColor),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchRepos,
child: const Text('重试'),
),
],
),
),
)
: _repos.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: AppTheme.secondaryTextColor,
),
SizedBox(height: 16),
Text('暂无仓库'),
],
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _repos.length,
itemBuilder: (context, index) {
final repo = _repos[index];
return _buildRepoCard(repo); // 构建仓库卡片
},
),
),
],
),
),
);
}
/// 构建仓库卡片
Widget _buildRepoCard(Map<String, dynamic> repo) {
return GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 仓库名称
Text(
repo['full_name'] ?? 'N/A',
style: const TextStyle(
fontSize: AppTheme.fontSizeLarge,
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
),
),
const SizedBox(height: 8),
// 仓库描述
Text(
repo['description'] ?? '无描述',
style: TextStyle(
fontSize: AppTheme.fontSizeMedium,
color: AppTheme.secondaryTextColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 16),
// 仓库信息(语言、星数、fork数)
Row(
children: [
// 语言
if (repo['language'] != null)
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getLanguageColor(repo['language']), // 语言颜色
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 6),
Text(
repo['language'] ?? '',
style: TextStyle(
fontSize: AppTheme.fontSizeSmall,
color: AppTheme.secondaryTextColor,
),
),
],
),
const SizedBox(width: 16),
// 星数
Row(
children: [
const Icon(
Icons.star_outline,
size: 16,
color: AppTheme.secondaryTextColor,
),
const SizedBox(width: 4),
Text(
(repo['stargazers_count'] ?? 0).toString(),
style: TextStyle(
fontSize: AppTheme.fontSizeSmall,
color: AppTheme.secondaryTextColor,
),
),
],
),
const SizedBox(width: 16),
// Fork数
Row(
children: [
const Icon(
Icons.fork_right_outlined,
size: 16,
color: AppTheme.secondaryTextColor,
),
const SizedBox(width: 4),
Text(
(repo['forks_count'] ?? 0).toString(),
style: TextStyle(
fontSize: AppTheme.fontSizeSmall,
color: AppTheme.secondaryTextColor,
),
),
],
),
],
),
],
),
);
}
/// 获取语言对应的颜色
Color _getLanguageColor(String? language) {
switch (language?.toLowerCase()) {
case 'dart':
return const Color(0xFF0175C2);
case 'python':
return const Color(0xFF3776AB);
case 'java':
return const Color(0xFFB07219);
case 'go':
return const Color(0xFF00ADD8);
case 'javascript':
return const Color(0xFFF7DF1E);
case 'typescript':
return const Color(0xFF3178C6);
case 'c++':
return const Color(0xFFF34B7D);
case 'c#':
return const Color(0xFF178600);
default:
return AppTheme.secondaryTextColor;
}
}
}
9. 个人中心(screens/profile_screen.dart)
作用:显示用户个人信息。
dart
// SPDX-License-Identifier: Apache-2.0
import './login_screen.dart';
import 'package:flutter/material.dart';
import '../services/gitcode_service.dart';
import '../themes/app_theme.dart';
import '../widgets/glass_card.dart';
/// 个人中心页面
class ProfileScreen extends StatefulWidget {
const ProfileScreen({Key? key}) : super(key: key);
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
/// GitCode服务实例
final GitCodeService _service = GitCodeService();
/// 用户信息
Map<String, dynamic>? _userInfo;
/// 加载状态
bool _isLoading = true;
/// 错误信息
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchUserInfo(); // 初始化时加载用户信息
}
/// 获取用户信息
Future<void> _fetchUserInfo() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final userInfo = await _service.fetchUserInfo();
setState(() {
_userInfo = userInfo;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的'),
backgroundColor: Colors.transparent,
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () {
// 打开设置页面(预留)
print('打开设置');
},
),
],
),
body: RefreshIndicator(
onRefresh: _fetchUserInfo, // 下拉刷新
color: AppTheme.primaryColor,
backgroundColor: AppTheme.cardBackgroundColor,
child: _isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppTheme.primaryColor),
SizedBox(height: 16),
Text('加载中...'),
],
),
)
: _errorMessage != null
? Center(
child: GlassCard(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.secondaryTextColor),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// 清除令牌并返回登录页
_service.clearToken();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
},
child: const Text('重新登录'),
),
],
),
),
)
: _userInfo == null
? const Center(
child: Text('未能获取用户信息'),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 用户信息卡片
GlassCard(
margin: const EdgeInsets.only(bottom: 24),
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 头像
CircleAvatar(
radius: 60,
backgroundImage: NetworkImage(_userInfo!['avatar_url'] ?? ''),
backgroundColor: AppTheme.secondaryColor.withOpacity(0.1),
),
const SizedBox(height: 16),
// 用户名
Text(
_userInfo!['name'] ?? _userInfo!['login'] ?? '匿名用户',
style: const TextStyle(
fontSize: AppTheme.fontSizeXXLarge,
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
),
),
// 登录名
Text(
'@${_userInfo!['login']}',
style: TextStyle(
fontSize: AppTheme.fontSizeLarge,
color: AppTheme.secondaryTextColor,
),
),
const SizedBox(height: 16),
// 简介
if (_userInfo!['bio'] != null)
Text(
_userInfo!['bio'] ?? '',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: AppTheme.fontSizeMedium,
color: AppTheme.secondaryTextColor,
),
),
],
),
),
// 统计信息卡片
GlassCard(
margin: const EdgeInsets.only(bottom: 24),
padding: const EdgeInsets.all(24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
'仓库',
_userInfo!['public_repos'] ?? 0,
Icons.code_outlined,
),
_buildStatItem(
'关注者',
_userInfo!['followers'] ?? 0,
Icons.people_outline,
),
_buildStatItem(
'关注中',
_userInfo!['following'] ?? 0,
Icons.person_add_outlined,
),
],
),
),
// 详细信息卡片
GlassCard(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'个人信息',
style: TextStyle(
fontSize: AppTheme.fontSizeXLarge,
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
),
),
const SizedBox(height: 16),
// 邮箱
if (_userInfo!['email'] != null)
_buildInfoItem(
Icons.email_outlined,
'邮箱',
_userInfo!['email'] ?? '未公开',
),
// 位置
if (_userInfo!['location'] != null)
_buildInfoItem(
Icons.location_on_outlined,
'位置',
_userInfo!['location'] ?? '未公开',
),
// 主页
if (_userInfo!['blog'] != null)
_buildInfoItem(
Icons.link_outlined,
'主页',
_userInfo!['blog'] ?? '未公开',
),
// 加入时间
_buildInfoItem(
Icons.calendar_today_outlined,
'加入时间',
_formatDate(_userInfo!['created_at']),
),
],
),
),
const SizedBox(height: 24),
// 登出按钮
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: () {
// 清除令牌并返回登录页
_service.clearToken();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
},
icon: const Icon(Icons.logout),
label: const Text('登出'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
),
),
),
],
),
),
),
);
}
/// 构建统计项
Widget _buildStatItem(String label, int count, IconData icon) {
return Column(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: AppTheme.primaryColor.withOpacity(0.1),
),
child: Icon(
icon,
size: 28,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
count.toString(),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppTheme.primaryTextColor,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: AppTheme.fontSizeSmall,
color: AppTheme.secondaryTextColor,
),
),
],
);
}
/// 构建信息项
Widget _buildInfoItem(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppTheme.secondaryColor.withOpacity(0.1),
),
child: Icon(
icon,
size: 20,
color: AppTheme.secondaryColor,
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: AppTheme.fontSizeSmall,
color: AppTheme.secondaryTextColor,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: AppTheme.fontSizeMedium,
color: AppTheme.primaryTextColor,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
/// 格式化日期
String _formatDate(String? dateStr) {
if (dateStr == null) return '';
final date = DateTime.parse(dateStr);
return '${date.year}年${date.month}月${date.day}日';
}
}
10. 入口文件(main.dart)
作用:应用的入口点,初始化主题和根组件。
dart
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import './screens/login_screen.dart';
import './themes/app_theme.dart';
/// 应用入口函数
void main() {
runApp(const MyApp()); // 启动应用
}
/// 根组件
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GitCode 客户端', // 应用标题
theme: AppTheme.themeData, // 应用主题
home: const LoginScreen(), // 初始页面(登录页)
debugShowCheckedModeBanner: false, // 隐藏调试横幅
);
}
}
三、毛玻璃二次元风格实现要点
- 背景渐变:使用柔和的粉紫色渐变作为背景,营造二次元氛围。
- 毛玻璃效果:通过
BackdropFilter+ImageFilter.blur实现卡片和导航栏的毛玻璃效果。 - 半透明卡片:卡片使用
Color.fromARGB(200, 255, 255, 255)半透明白色背景,增强玻璃感。 - 主题色搭配:采用粉紫、天蓝、樱花粉等二次元常用颜色,形成统一的视觉风格。
- 圆角设计:所有卡片和组件使用较大的圆角(16px),增强柔和感。
- 阴影效果:使用主题色的淡阴影,增加层次感。
四、运行与测试
-
添加依赖:在
pubspec.yaml中添加dio依赖:yaml
dependencies: flutter: sdk: flutter dio: ^5.7.0 flutter_easyloading: ^3.0.5 # 添加这个 shared_preferences: ^2.3.0
五、效果图:


更多推荐



所有评论(0)