bill_status_bar.dart 7.8 KB

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