前言:本文章是我在看到其他人的文章后的学习笔记,我参照的文章链接在下面:

【学习打卡】day03基于flutter实现鸿蒙应用开发之gitcode口袋工具箱完善版本-CSDN博客https://blog.csdn.net/laoren1998/article/details/155579537?fromshare=blogdetail&sharetype=blogdetail&sharerId=155579537&sharerefer=PC&sharesource=jhp0108&sharefrom=from_link

一、项目核心定位

GitCode口袋工具是基于Flutter开发、适配DevEco Studio的轻量化GitCode平台客户端,核心亮点的是毛玻璃二次元视觉风格,功能覆盖登录验证、动态查看、仓库管理、个人信息展示,整体架构模块化,便于维护与扩展。

二、前期准备

  1. 开发环境配置 - 开发工具:DevEco Studio(需适配Flutter开发插件) - 技术依赖:在项目 pubspec.yaml 中添加核心依赖,支撑网络请求、本地存储等功能: yaml dependencies: flutter: sdk: flutter dio: ^5.7.0 # 网络请求核心库 shared_preferences: ^2.3.0 # 本地存储(令牌管理) flutter_easyloading: ^3.0.5 # 加载状态提示(可选优化) - 权限配置:确保项目开启网络权限,满足GitCode API请求需求。

  2. 项目结构规划

采用模块化拆分思路,明确各目录职责,降低代码耦合度,结构如下:

plaintext

lib/

├── main.dart # 应用入口,初始化主题与根组件

├── constants/ # 常量集中管理(API地址、存储键等)

       └── app_constants.dart # 核心常量定义

├── themes/ # 主题配置专属目录

       └── app_theme.dart # 毛玻璃二次元主题核心配置

├── services/ # 网络服务层

       └── gitcode_service.dart # GitCode API请求封装(单例模式)

├── 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基础地址、令牌存储键、历史记录限制等常量,避免硬编码,便于统一修改: 

    class AppConstants {
      // GitCode API基础地址
      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) 主题是风格落地核心,统一管控颜色、字体、组件样式,重点实现二次元视觉与毛玻璃适配: 

    import 'package:flutter/material.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(0xC8FFFFFF); 
     /// 文字主色
     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) 全局复用核心组件,通过 BackdropFilter + ImageFilter.blur 实现毛玻璃效果,适配各页面:

    // 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) 采用单例模式封装GitCode API请求,统一处理令牌添加、错误拦截,支撑核心功能: 

    // 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目录) (1)登录页(login_screen.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),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }

    (2)主页面(main_screen.dart) 底部导航核心页,切换首页、仓库、个人中心,导航栏适配毛玻璃效果:

    import 'dart:ui';
    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 = [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(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,
                backgroundColor: AppTheme.cardBackgroundColor,
                selectedItemColor: AppTheme.primaryColor,
                unselectedItemColor: AppTheme.secondaryTextColor,
                elevation: 8,
                type: BottomNavigationBarType.fixed,
              ),
            ),
          ),
        );
      }
    }
    

    (3)首页动态/仓库/个人中心页 均基于 GlassCard 组件复用毛玻璃风格,核心逻辑为:请求接口数据→展示加载/错误/空状态→渲染列表/详情,以个人中心为例(展示用户信息+登出功能): 

    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> {
      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);
        }
      }
    
      // 格式化日期(YYYY年MM月DD日)
      String _formatDate(String? dateStr) => dateStr == null ? '' : DateTime.parse(dateStr).toString().split(' ')[0].replaceAll('-', '年') + '日';
    
      // 构建统计项(仓库/关注者/关注中)
      Widget _buildStatItem(String label, int count, IconData icon) => 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: bold, color: AppTheme.primaryTextColor)),
              const SizedBox(height: 4),
              Text(label, style: TextStyle(fontSize: AppTheme.fontSizeSmall, color: AppTheme.secondaryTextColor)),
            ],
          );
    
      @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),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.center,
                              children: [
                                // 用户信息卡片
                                GlassCard(
                                  margin: const EdgeInsets.only(bottom: 24),
                                  padding: const EdgeInsets.all(24),
                                  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: 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),
                                  child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
                                    _buildStatItem('仓库', _userInfo!['public_repos'] ?? 0, Icons.code_outlined),
                                    _buildStatItem('关注者', _userInfo!['followers'] ?? 0, Icons.people_outlined),
                                    _buildStatItem('关注中', _userInfo!['following'] ?? 0, Icons.person_add_outlined),
                                  ]),
                                ),
                                // 登出按钮(红色主题,区分常规按钮)
                                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),
                                  ),
                                ),
                              ],
                            ),
                          ),
          ),
        );
      }
    }
     

  6. 应用入口(main.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, // 隐藏调试横幅
    );
  }
}

四、毛玻璃二次元风格核心要点

1. 背景基础:全局使用 LinearGradient 浅紫灰→浅蓝紫渐变,奠定柔和二次元基调,避免纯色单调。

2. 毛玻璃核心:通过 BackdropFilter + ImageFilter.blur(sigmaX:10, sigmaY:10) 实现模糊效果,配合半透明白卡片( Color.fromARGB(200, 255, 255, 255) ),强化玻璃质感。

3. 视觉细节:

- 圆角:全局统一16px圆角,弱化尖锐感,契合二次元柔和风格;

- 配色:以粉紫为主色,天蓝、樱花粉为辅,文字区分主次色,视觉统一;

- 阴影:使用主题色淡阴影( opacity:0.1 ),增加组件层次感,不突兀。

4. 组件复用:封装 GlassCard 组件,全局页面复用,确保风格一致性,减少重复代码。

五、运行测试

1. 依赖安装:配置 pubspec.yaml 后,点击DevEco Studio「Pub get」安装依赖;

2. 设备适配:选择模拟器或真实设备,确保设备网络正常;

3. 功能测试:验证登录(令牌有效性)、动态加载、仓库切换、个人信息展示、登出等核心功能,检查毛玻璃效果适配性。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐