bill_status_bar.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import '../../core/theme/app_colors_extension.dart';
  4. /// 单据状态标签 + 底部操作按钮。
  5. ///
  6. /// 根据 [billStatus] 中的 `approvalStatus`(SHCOUNT0~6)、
  7. /// `isClosed`、`isTransferred`、`isCreator`、`canEdit` 控制展示。
  8. ///
  9. /// 使用方式:
  10. /// ```dart
  11. /// BillStatusBar(
  12. /// billStatus: _billStatus,
  13. /// onEdit: () => context.push('/xxx/edit/$billNo'),
  14. /// onSubmit: () => api.shSubmit(bilNo: billNo, bilDd: ...),
  15. /// onCancelSubmit: () => api.shSubmit(bilNo: billNo, bilDd: ..., isCancel: true),
  16. /// onTapStatusTag: () => _showAuditTrail(),
  17. /// ),
  18. /// ```
  19. class BillStatusBar {
  20. /// getBillStatus 接口返回的完整 Map,为 null 时显示空状态。
  21. final Map<String, dynamic>? billStatus;
  22. /// "修改"按钮回调
  23. final VoidCallback? onEdit;
  24. /// "提交"按钮回调(shSubmit)
  25. final VoidCallback? onSubmit;
  26. /// "取消提交"按钮回调(shSubmit + isCancel)
  27. final VoidCallback? onCancelSubmit;
  28. /// 点击状态标签回调(查看审批轨迹)
  29. final VoidCallback? onTapStatusTag;
  30. /// 已关闭状态文本(如"已关闭",仅 expense 使用)
  31. final String? closedText;
  32. /// 已转单状态文本(如"已转为报销单",expense/expense_apply 使用)
  33. final String? transferredText;
  34. const BillStatusBar({
  35. this.billStatus,
  36. this.onEdit,
  37. this.onSubmit,
  38. this.onCancelSubmit,
  39. this.onTapStatusTag,
  40. this.closedText,
  41. this.transferredText,
  42. });
  43. // ── 派生状态 ──
  44. bool get _isClosed => billStatus?['isClosed'] == true;
  45. bool get _isTransferred => billStatus?['isTransferred'] == true;
  46. String? get _approvalStatus => billStatus?['approvalStatus'] as String?;
  47. String? get _approvalText => billStatus?['approvalText'] as String?;
  48. bool get _canEdit => billStatus?['canEdit'] == true;
  49. // ── 按钮显隐 ──
  50. bool get showEdit {
  51. final s = _approvalStatus;
  52. if (s == 'SHCOUNT0' || s == 'SHCOUNT3') return _canEdit;
  53. if (s == 'SHCOUNT6') return _canEdit;
  54. return false;
  55. }
  56. bool get showSubmit {
  57. final s = _approvalStatus;
  58. return s == 'SHCOUNT0' || s == 'SHCOUNT3';
  59. }
  60. bool get showCancelSubmit => _approvalStatus == 'SHCOUNT1';
  61. bool get _showButtons => showEdit || showSubmit || showCancelSubmit;
  62. // ── 状态标签数据 ──
  63. ({IconData icon, Color color}) _statusStyle() {
  64. if (_isClosed) {
  65. return (icon: Icons.lock_outline, color: Colors.blueGrey);
  66. }
  67. if (_isTransferred) {
  68. return (icon: Icons.swap_horiz, color: Colors.blue);
  69. }
  70. final s = _approvalStatus;
  71. if (s == null || s.isEmpty) {
  72. return (icon: Icons.help_outline, color: Colors.grey);
  73. }
  74. switch (s) {
  75. case 'SHCOUNT0':
  76. return (icon: Icons.edit_note, color: Colors.teal);
  77. case 'SHCOUNT1':
  78. return (icon: Icons.hourglass_empty, color: Colors.orange);
  79. case 'SHCOUNT2':
  80. return (icon: Icons.cancel_outlined, color: Colors.red);
  81. case 'SHCOUNT3':
  82. return (icon: Icons.undo, color: Colors.deepOrange);
  83. case 'SHCOUNT4':
  84. return (icon: Icons.check_circle_outline, color: Colors.green);
  85. case 'SHCOUNT5':
  86. return (icon: Icons.sync, color: Colors.blue);
  87. case 'SHCOUNT6':
  88. return (icon: Icons.verified, color: Colors.indigo);
  89. default:
  90. return (icon: Icons.help_outline, color: Colors.grey);
  91. }
  92. }
  93. String? get _displayText {
  94. if (_isClosed) return closedText;
  95. if (_isTransferred) return transferredText;
  96. return _approvalText;
  97. }
  98. // ── 构建 ──
  99. /// 仅状态标签(用于 FormSection trailing)
  100. Widget buildStatusTag(BuildContext context) {
  101. final style = _statusStyle();
  102. final text = _displayText;
  103. if (text == null || text.isEmpty) return const SizedBox.shrink();
  104. final tag = Container(
  105. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
  106. decoration: BoxDecoration(
  107. color: style.color.withValues(alpha: 0.1),
  108. borderRadius: BorderRadius.circular(6),
  109. border: Border.all(
  110. color: style.color.withValues(alpha: 0.3),
  111. width: 0.5,
  112. ),
  113. ),
  114. child: Row(
  115. mainAxisSize: MainAxisSize.min,
  116. children: [
  117. Icon(style.icon, size: 18, color: style.color),
  118. const SizedBox(width: 4),
  119. Text(
  120. text,
  121. style: TextStyle(
  122. fontSize: 13,
  123. fontWeight: FontWeight.w600,
  124. color: style.color,
  125. ),
  126. ),
  127. ],
  128. ),
  129. );
  130. if (onTapStatusTag != null) {
  131. return GestureDetector(onTap: onTapStatusTag, child: tag);
  132. }
  133. return tag;
  134. }
  135. /// 底部操作按钮区
  136. Widget buildActions(BuildContext context) {
  137. if (!_showButtons) return const SizedBox.shrink();
  138. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  139. final bottomPadding = MediaQuery.of(context).padding.bottom;
  140. return Container(
  141. padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomPadding),
  142. decoration: BoxDecoration(
  143. color: colors.bgCard,
  144. border: Border(top: BorderSide(color: colors.border)),
  145. ),
  146. child: Row(
  147. children: [
  148. if (showEdit) ...[
  149. Expanded(
  150. child: TDButton(
  151. icon: TDIcons.edit,
  152. text: _editLabel(),
  153. size: TDButtonSize.medium,
  154. type: TDButtonType.outline,
  155. shape: TDButtonShape.rectangle,
  156. theme: TDButtonTheme.primary,
  157. onTap: onEdit,
  158. ),
  159. ),
  160. ],
  161. if (showEdit && (showSubmit || showCancelSubmit))
  162. const SizedBox(width: 12),
  163. if (showSubmit)
  164. Expanded(
  165. child: TDButton(
  166. icon: TDIcons.send,
  167. text: _submitLabel(),
  168. size: TDButtonSize.medium,
  169. type: TDButtonType.fill,
  170. shape: TDButtonShape.rectangle,
  171. theme: TDButtonTheme.primary,
  172. onTap: onSubmit,
  173. ),
  174. ),
  175. if (showCancelSubmit)
  176. Expanded(
  177. child: TDButton(
  178. icon: TDIcons.rollback,
  179. text: _cancelSubmitLabel(),
  180. size: TDButtonSize.medium,
  181. type: TDButtonType.fill,
  182. shape: TDButtonShape.rectangle,
  183. theme: TDButtonTheme.danger,
  184. onTap: onCancelSubmit,
  185. ),
  186. ),
  187. ],
  188. ),
  189. );
  190. }
  191. // ── 按钮标签 ──
  192. /// 默认返回中文按钮标签(不依赖 i18n,简化跨模块复用)
  193. static String _editLabel() => '修改';
  194. static String _submitLabel() => '提交';
  195. static String _cancelSubmitLabel() => '取消提交';
  196. }