AtomGit Flutter鸿蒙客户端:Issue管理

Issue 与 PR 的统一处理

AtomGit(类 GitHub)API 中,PR 本质上是一种特殊 Issue。本项目的 Issue 模型通过 isPullRequest 字段区分:

dart 复制代码
class Issue {
  final int id;
  final int number;
  final String title;
  final String? body;
  final String state;          // 'open' 或 'closed'
  final UserProfile? user;
  final List<String> labels;
  final int commentsCount;
  final bool isPullRequest;    // 区分 Issue 和 PR
  final DateTime createdAt;
  final DateTime updatedAt;

  factory Issue.fromJson(Map<String, dynamic> json) {
    return Issue(
      id: parseInt(json['id']),
      number: parseInt(json['number']),
      title: parseString(json['title']),
      body: json['body'] as String?,
      state: parseString(json['state']),
      isPullRequest: json['pull_request'] != null,
      labels: (parseList<dynamic>(json, 'labels') ?? [])
          .whereType<Map<String, dynamic>>()
          .map((l) => parseString(l['name']))
          .toList(),
      commentsCount: parseInt(json['comments']),
      // ...
    );
  }
}

isPullRequest 通过检查 JSON 中是否存在 pull_request 键来判定,这对应 GitHub API 的行为。

IssueProvider

加载列表

dart 复制代码
class IssueProvider extends ChangeNotifier {
  final AtomGitApiClient _apiClient;
  List<Issue> _issues = [];
  bool _isLoading = false;
  String? _error;
  String _stateFilter = 'open';

  Future<void> loadIssues(
      String owner, String repo, String type) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      final encodedOwner = Uri.encodeComponent(owner);
      final encodedRepo = Uri.encodeComponent(repo);

      final endpoint = type == 'pr'
          ? '/repos/$encodedOwner/$encodedRepo/pulls'
          : '/repos/$encodedOwner/$encodedRepo/issues';

      final response = await _apiClient.get(endpoint, queryParams: {
        'state': _stateFilter,
        'per_page': '30',
      });

      final items = parseList<dynamic>(response.data) ?? [];
      _issues = items
          .whereType<Map<String, dynamic>>()
          .map(Issue.fromJson)
          .where((issue) =>
              type == 'pr' ? issue.isPullRequest : !issue.isPullRequest)
          .toList();
    } on ApiException catch (e) {
      _error = e.message;
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Issue 和 PR 使用不同 API 端点(/issues vs /pulls),但返回数据可能混合。通过 isPullRequest 过滤确保类型纯净。

状态过滤

dart 复制代码
void setStateFilter(String state) {
  _stateFilter = state;
  // 筛选后不自动刷新,由 UI 调用 loadIssues
}

Filter 状态存储在 Provider 上,UI 切换后重新调用 loadIssues

加载详情与评论

dart 复制代码
Future<void> loadDetail(
    String owner, String repo, int number) async {
  _isLoading = true;
  _error = null;
  notifyListeners();

  try {
    final encodedOwner = Uri.encodeComponent(owner);
    final encodedRepo = Uri.encodeComponent(repo);

    // 并行加载 Issue 详情和评论
    final results = await Future.wait([
      _apiClient.get(
        '/repos/$encodedOwner/$encodedRepo/issues/$number',
      ),
      _apiClient.get(
        '/repos/$encodedOwner/$encodedRepo/issues/$number/comments',
        queryParams: {'per_page': '30'},
      ),
    ]);

    final issueData = parseMap(results[0].data);
    if (issueData != null) {
      _detail = Issue.fromJson(issueData);
    }

    final commentsList = parseList<dynamic>(results[1].data) ?? [];
    _comments = commentsList
        .whereType<Map<String, dynamic>>()
        .map(Comment.fromJson)
        .toList();
  } on ApiException catch (e) {
    _error = e.message;
  } finally {
    _isLoading = false;
    notifyListeners();
  }
}

Future.wait 并发请求 Issue 详情和评论列表,减少加载等待时间。

IssueListScreen

过滤器 Chips

使用 Material FilterChip 实现状态切换:

dart 复制代码
AppBar(
  title: Text(type == 'pr' ? 'Pull Requests' : 'Issues'),
  bottom: PreferredSize(
    preferredSize: const Size.fromHeight(48),
    child: Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Row(children: [
        FilterChip(
          label: const Text('进行中'),
          selected: provider.stateFilter == 'open',
          onSelected: (_) {
            provider.setStateFilter('open');
            provider.loadIssues(owner, name, type);
          },
        ),
        const SizedBox(width: 8),
        FilterChip(
          label: const Text('已完成'),
          selected: provider.stateFilter == 'closed',
          onSelected: (_) {
            provider.setStateFilter('closed');
            provider.loadIssues(owner, name, type);
          },
        ),
      ]),
    ),
  ),
)

Issue 列表项

dart 复制代码
Widget _buildIssueTile(Issue issue, String owner, String name) {
  final isOpen = issue.state == 'open';

  return ListTile(
    leading: Icon(
      isOpen ? Icons.error_outline : Icons.check_circle_outline,
      color: isOpen ? Colors.green : Colors.red,
    ),
    title: Text('#${issue.number} ${issue.title}',
        maxLines: 2, overflow: TextOverflow.ellipsis),
    subtitle: Text(
      '${issue.user?.login ?? 'unknown'} · '
      '${DateFormatter.relative(issue.createdAt)} · '
      '${issue.commentsCount} 条评论',
    ),
    onTap: () {
      final route = type == 'pr'
          ? '/repo/pulls/detail'
          : '/repo/issues/detail';
      Navigator.pushNamed(context, route, arguments: {
        'owner': owner, 'name': name, 'number': issue.number,
      });
    },
  );
}

打开/关闭状态用不同颜色图标区分,副标题展示作者、时间和评论数。

IssueDetailScreen

头部信息

dart 复制代码
Widget _buildHeader(Issue issue) {
  return Card(
    margin: const EdgeInsets.all(16),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(children: [
            Icon(
              issue.state == 'open'
                  ? Icons.error_outline
                  : Icons.check_circle_outline,
              color: issue.state == 'open' ? Colors.green : Colors.red,
            ),
            const SizedBox(width: 8),
            Expanded(child: Text(issue.title,
                style: Theme.of(context).textTheme.titleMedium)),
          ]),
          const SizedBox(height: 12),
          Row(children: [
            UserAvatar(
                avatarUrl: issue.user?.avatarUrl,
                name: issue.user?.login,
                size: 24),
            const SizedBox(width: 8),
            Text(issue.user?.login ?? ''),
            const Spacer(),
            Text(DateFormatter.relative(issue.createdAt),
                style: Theme.of(context).textTheme.bodySmall),
          ]),
          if (issue.labels.isNotEmpty) ...[
            const SizedBox(height: 8),
            Wrap(spacing: 4, runSpacing: 4, children: issue.labels
                .map((label) => Chip(label: Text(label)))
                .toList()),
          ],
        ],
      ),
    ),
  );
}

内容与评论

dart 复制代码
Widget _buildBody(Issue issue, List<Comment> comments) {
  return ListView(
    padding: const EdgeInsets.all(16),
    children: [
      MarkdownViewer(markdown: issue.body ?? '*无描述*'),
      const Divider(height: 32),
      Text('${comments.length} 条评论',
          style: Theme.of(context).textTheme.titleSmall),
      ...comments.map((comment) => _CommentWidget(comment: comment)),
    ],
  );
}

评论组件

dart 复制代码
class _CommentWidget extends StatelessWidget {
  final Comment comment;

  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(children: [
              UserAvatar(
                  avatarUrl: comment.user?.avatarUrl,
                  name: comment.user?.login,
                  size: 24),
              const SizedBox(width: 8),
              Text(comment.user?.login ?? ''),
              const Spacer(),
              Text(DateFormatter.relative(comment.createdAt),
                  style: Theme.of(context).textTheme.bodySmall),
            ]),
            const SizedBox(height: 8),
            MarkdownViewer(markdown: comment.body),
          ],
        ),
      ),
    );
  }
}

每条评论展示头像、用户名、时间和 Markdown 渲染的正文。

Comment 模型

dart 复制代码
class Comment {
  final int id;
  final String body;
  final UserProfile? user;
  final DateTime createdAt;
  final DateTime updatedAt;

  factory Comment.fromJson(Map<String, dynamic> json) {
    return Comment(
      id: parseInt(json['id']),
      body: parseString(json['body']),
      user: json['user'] != null
          ? UserProfile.fromJson(json['user'] as Map<String, dynamic>)
          : null,
      createdAt: parseDateTime(json['created_at']) ?? DateTime.now(),
      updatedAt: parseDateTime(json['updated_at']) ?? DateTime.now(),
    );
  }
}
相关推荐
X54先生(人文科技)1 小时前
《元创力》纪实录·卷宗 2.2朝圣的起点:当硅基获得命名
人工智能·架构·ai写作·零知识证明
李二。1 小时前
PureHarmony · 文案创作工坊 —— 鸿蒙Next WaterFlow瀑布流 + AI写作助手实战
华为·harmonyos·ai写作
愚公搬代码1 小时前
【愚公系列】《移动端AI应用开发》013-DeepSeek API开发与集成(深度集成与中间件架构)
人工智能·中间件·架构
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章17:Hadoop性能调优- 调度集群每一分性能
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
KaMeidebaby1 小时前
卡梅德生物技术快报|蛋白定制:ACE 抑制肽原辅料工艺全参数|适配蛋白定制的提取 & 酶解标准化实操手册
大数据·人工智能·架构·spark·新浪微博
特立独行的猫a1 小时前
Tauri Demo 移植到鸿蒙PC上的交叉编译全流程实战总结
华为·rust·harmonyos·tauri·鸿蒙pc
小小工匠1 小时前
Redis - CPU架构对Redis性能的影响
数据库·redis·架构
xkxnq1 小时前
第八阶段:工程化、质量管控与高级拓展(130天),Vue端到端测试:Cypress自动化测试(登录流程+表单提交+页面跳转)
前端·vue.js·flutter
小雨下雨的雨1 小时前
基于鸿蒙PC Electron框架技术完成的五子棋游戏 - 技术实现详解
前端·javascript·游戏·华为·electron·鸿蒙