approval_timeline.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import 'package:flutter/material.dart';
  2. import '../../core/theme/app_colors.dart';
  3. import '../models/approval_status.dart';
  4. class ApprovalTimeline extends StatelessWidget {
  5. final List<ApprovalRecord> records;
  6. final List<String> chain;
  7. final String currentApproverId;
  8. const ApprovalTimeline({
  9. super.key,
  10. required this.records,
  11. required this.chain,
  12. this.currentApproverId = '',
  13. });
  14. @override
  15. Widget build(BuildContext context) {
  16. return Column(
  17. crossAxisAlignment: CrossAxisAlignment.start,
  18. children: [
  19. const Text('审批进度',
  20. style: TextStyle(
  21. fontSize: 13,
  22. fontWeight: FontWeight.w600,
  23. color: AppColors.textPrimary)),
  24. const SizedBox(height: 12),
  25. ...chain.asMap().entries.map((entry) {
  26. final idx = entry.key;
  27. final approverId = entry.value;
  28. final record = records.where((r) => r.approverId == approverId).firstOrNull;
  29. final isCurrent = approverId == currentApproverId;
  30. return _buildNode(idx, chain.length, approverId, record, isCurrent);
  31. }),
  32. ],
  33. );
  34. }
  35. Widget _buildNode(int index, int total, String approverId,
  36. ApprovalRecord? record, bool isCurrent) {
  37. final isDone = record != null && record.action == 'approve';
  38. final isRejected = record != null && record.action == 'reject';
  39. final isLast = index == total - 1;
  40. return IntrinsicHeight(
  41. child: Row(
  42. crossAxisAlignment: CrossAxisAlignment.start,
  43. children: [
  44. Column(
  45. children: [
  46. Container(
  47. width: 22,
  48. height: 22,
  49. decoration: BoxDecoration(
  50. color: isRejected ? AppColors.error :
  51. isDone ? AppColors.success :
  52. isCurrent ? AppColors.primary :
  53. const Color(0xFFDDDDDD),
  54. shape: BoxShape.circle,
  55. ),
  56. child: Center(
  57. child: isRejected ? const Icon(Icons.close, size: 12, color: Colors.white) :
  58. isDone ? const Icon(Icons.check, size: 12, color: Colors.white) :
  59. isCurrent ? const Text('●', style: TextStyle(color: Colors.white, fontSize: 10)) :
  60. null,
  61. ),
  62. ),
  63. if (!isLast)
  64. Container(
  65. width: 1.5,
  66. height: 36,
  67. color: isDone ? AppColors.success : const Color(0xFFDDDDDD)),
  68. ],
  69. ),
  70. const SizedBox(width: 10),
  71. Expanded(
  72. child: Padding(
  73. padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
  74. child: Column(
  75. crossAxisAlignment: CrossAxisAlignment.start,
  76. children: [
  77. Text(
  78. record?.approverName ?? '审批人$approverId',
  79. style: TextStyle(
  80. fontSize: 13,
  81. fontWeight: FontWeight.w500,
  82. color: isRejected ? AppColors.error :
  83. isCurrent ? AppColors.primary :
  84. AppColors.textPrimary,
  85. ),
  86. ),
  87. if (record != null && record.action != 'pending') ...[
  88. const SizedBox(height: 2),
  89. Text(
  90. '${record.action == 'approve' ? '已通过' : '已拒绝'} · ${record.approvalTime.toString().substring(0, 16)}',
  91. style: const TextStyle(fontSize: 11, color: AppColors.textHint),
  92. ),
  93. if (record.opinion.isNotEmpty)
  94. Padding(
  95. padding: const EdgeInsets.only(top: 2),
  96. child: Text('意见:${record.opinion}',
  97. style: const TextStyle(fontSize: 11, color: AppColors.textSecondary)),
  98. ),
  99. ] else if (isCurrent) ...[
  100. const SizedBox(height: 2),
  101. const Text('当前节点',
  102. style: TextStyle(fontSize: 11, color: AppColors.primary)),
  103. ] else ...[
  104. const SizedBox(height: 2),
  105. const Text('待处理',
  106. style: TextStyle(fontSize: 11, color: AppColors.textHint)),
  107. ],
  108. ],
  109. ),
  110. ),
  111. ),
  112. ],
  113. ),
  114. );
  115. }
  116. }