audit_trail_dialog.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import '../../core/theme/app_colors.dart';
  4. import '../../core/theme/app_colors_extension.dart';
  5. import '../../core/i18n/app_localizations.dart';
  6. /// 审核过程明细弹窗
  7. ///
  8. /// 调用 [AuditTrailDialog.show] 弹出,自动拉取数据并展示。
  9. class AuditTrailDialog extends StatelessWidget {
  10. final Future<List<Map<String, dynamic>>> Function() fetcher;
  11. const AuditTrailDialog({super.key, required this.fetcher});
  12. /// 显示弹窗,[fetcher] 负责调 API 获取数据。
  13. static Future<void> show(
  14. BuildContext context, {
  15. required Future<List<Map<String, dynamic>>> Function() fetcher,
  16. }) {
  17. return Navigator.push(
  18. context,
  19. TDSlidePopupRoute(
  20. slideTransitionFrom: SlideTransitionFrom.bottom,
  21. builder: (_) => AuditTrailDialog(fetcher: fetcher),
  22. ),
  23. );
  24. }
  25. @override
  26. Widget build(BuildContext context) {
  27. final l10n = AppLocalizations.of(context);
  28. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  29. return FutureBuilder<List<Map<String, dynamic>>>(
  30. future: fetcher(),
  31. builder: (ctx, snapshot) {
  32. return SafeArea(
  33. child: SizedBox(
  34. height: MediaQuery.of(context).size.height * 0.8,
  35. child: Container(
  36. decoration: BoxDecoration(
  37. color: colors.bgPage,
  38. borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
  39. ),
  40. child: Column(
  41. children: [
  42. Padding(
  43. padding: const EdgeInsets.fromLTRB(20, 16, 12, 12),
  44. child: Row(
  45. children: [
  46. Text(
  47. l10n.get('auditTrail'),
  48. style: TextStyle(fontSize: AppFontSizes.title, fontWeight: FontWeight.w600, color: colors.textPrimary),
  49. ),
  50. const Spacer(),
  51. GestureDetector(
  52. onTap: () => Navigator.pop(context),
  53. child: Icon(Icons.close, size: 20, color: colors.textSecondary),
  54. ),
  55. ],
  56. ),
  57. ),
  58. Divider(height: 1, color: colors.border),
  59. Flexible(
  60. child: _buildContent(snapshot, colors),
  61. ),
  62. ],
  63. ),
  64. ),
  65. ),
  66. );
  67. },
  68. );
  69. }
  70. Widget _buildContent(AsyncSnapshot<List<Map<String, dynamic>>> snapshot, AppColorsExtension colors) {
  71. if (snapshot.connectionState == ConnectionState.waiting) {
  72. return const Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity));
  73. }
  74. if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
  75. return const Center(child: Text('—', style: TextStyle(fontSize: 14, color: Color(0xFF999999))));
  76. }
  77. final items = snapshot.data!;
  78. return ListView.builder(
  79. shrinkWrap: true,
  80. padding: const EdgeInsets.all(16),
  81. itemCount: items.length,
  82. itemBuilder: (_, i) {
  83. final item = items[i];
  84. final action = item['action']?.toString() ?? '';
  85. Color iconColor;
  86. IconData icon;
  87. if (action == 'Y') {
  88. icon = Icons.check_circle;
  89. iconColor = colors.success;
  90. } else if (action == 'N') {
  91. icon = Icons.cancel;
  92. iconColor = colors.danger;
  93. } else {
  94. icon = Icons.hourglass_empty;
  95. iconColor = colors.infoText;
  96. }
  97. return Padding(
  98. padding: const EdgeInsets.only(bottom: 12),
  99. child: Column(
  100. crossAxisAlignment: CrossAxisAlignment.start,
  101. children: [
  102. Row(
  103. crossAxisAlignment: CrossAxisAlignment.start,
  104. children: [
  105. Icon(icon, size: 18, color: iconColor),
  106. const SizedBox(width: 8),
  107. Expanded(
  108. child: Column(
  109. crossAxisAlignment: CrossAxisAlignment.start,
  110. children: [
  111. Row(
  112. children: [
  113. Expanded(child: Text(item['user']?.toString() ?? '', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.textPrimary))),
  114. Text(item['date']?.toString() ?? '', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
  115. ],
  116. ),
  117. if ((item['node']?.toString() ?? '').isNotEmpty) ...[
  118. const SizedBox(height: 2),
  119. Text(item['node']?.toString() ?? '', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  120. ],
  121. ],
  122. ),
  123. ),
  124. ],
  125. ),
  126. if ((item['remark']?.toString() ?? '').isNotEmpty) ...[
  127. const SizedBox(height: 4),
  128. Padding(
  129. padding: const EdgeInsets.only(left: 26),
  130. child: Text(item['remark']?.toString() ?? '', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
  131. ),
  132. ],
  133. ],
  134. ),
  135. );
  136. },
  137. );
  138. }
  139. }