bill_status_bar.dart 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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? onEditReturn;
  33. /// 点击状态标签回调(查看审批轨迹)
  34. final VoidCallback? onTapStatusTag;
  35. /// 已关闭状态文本(如"已关闭",仅 expense 使用)
  36. final String? closedText;
  37. /// 已转单状态文本(如"已转为报销单",expense/expense_apply 使用)
  38. final String? transferredText;
  39. /// 已还车状态文本(如"已还车",仅用车申请单使用,优先级最高)
  40. final String? returnedText;
  41. /// 是否允许手动提交审核流。为 false 时提交/取消提交按钮不显示。
  42. /// 当 ERP 已自动处理存盘立即提交时,无需在详情页再次手动操作。
  43. final bool canManualSubmit;
  44. const BillStatusBar({
  45. this.billStatus,
  46. this.onEdit,
  47. this.onSubmit,
  48. this.onCancelSubmit,
  49. this.onReturn,
  50. this.onEditReturn,
  51. this.onTapStatusTag,
  52. this.closedText,
  53. this.transferredText,
  54. this.returnedText,
  55. this.canManualSubmit = true,
  56. });
  57. // ── 派生状态 ──
  58. bool get _isClosed => billStatus?['isClosed'] == true;
  59. bool get _isTransferred => billStatus?['isTransferred'] == true;
  60. bool get _isReturned {
  61. final extras = billStatus?['extras'] as Map<String, dynamic>?;
  62. return extras?['isReturned'] == true;
  63. }
  64. String? get _approvalStatus => billStatus?['approvalStatus'] as String?;
  65. String? get _approvalText => billStatus?['approvalText'] as String?;
  66. bool get _canEdit => billStatus?['canEdit'] == true;
  67. // ── 按钮显隐 ──
  68. bool get showEdit {
  69. if (_isReturned) return false;
  70. final s = _approvalStatus;
  71. if (s == 'SHCOUNT0' || s == 'SHCOUNT3') return _canEdit;
  72. if (s == 'SHCOUNT6') return _canEdit;
  73. return false;
  74. }
  75. bool get showSubmit {
  76. if (!canManualSubmit) return false;
  77. if (_isClosed || _isTransferred || _isReturned) return false;
  78. final s = _approvalStatus;
  79. return s == 'SHCOUNT0' || s == 'SHCOUNT3';
  80. }
  81. bool get showCancelSubmit {
  82. if (!canManualSubmit) return false;
  83. if (_isClosed || _isTransferred || _isReturned) return false;
  84. return _approvalStatus == 'SHCOUNT1';
  85. }
  86. bool get showReturn {
  87. if (onReturn == null) return false;
  88. if (_isReturned) return false;
  89. final s = _approvalStatus;
  90. return (s == 'SHCOUNT4' || s == 'SHCOUNT6') &&
  91. !_isClosed &&
  92. !_isTransferred;
  93. }
  94. bool get showEditReturn {
  95. if (onEditReturn == null) return false;
  96. return _isReturned;
  97. }
  98. bool get _showButtons =>
  99. showEdit || showSubmit || showCancelSubmit || showReturn || showEditReturn;
  100. // ── 状态标签数据 ──
  101. ({IconData icon, Color color}) _statusStyle() {
  102. if (_isReturned) {
  103. return (icon: Icons.task_alt, color: Colors.deepPurple);
  104. }
  105. if (_isClosed) {
  106. return (icon: Icons.lock_outline, color: Colors.blueGrey);
  107. }
  108. if (_isTransferred) {
  109. return (icon: Icons.swap_horiz, color: Colors.blue);
  110. }
  111. final s = _approvalStatus;
  112. if (s == null || s.isEmpty) {
  113. return (icon: Icons.help_outline, color: Colors.grey);
  114. }
  115. switch (s) {
  116. case 'SHCOUNT0':
  117. return (icon: Icons.edit_note, color: Colors.teal);
  118. case 'SHCOUNT1':
  119. return (icon: Icons.hourglass_empty, color: Colors.orange);
  120. case 'SHCOUNT2':
  121. return (icon: Icons.cancel_outlined, color: Colors.red);
  122. case 'SHCOUNT3':
  123. return (icon: Icons.undo, color: Colors.deepOrange);
  124. case 'SHCOUNT4':
  125. return (icon: Icons.check_circle_outline, color: Colors.green);
  126. case 'SHCOUNT5':
  127. return (icon: Icons.sync, color: Colors.blue);
  128. case 'SHCOUNT6':
  129. return (icon: Icons.verified, color: Colors.indigo);
  130. default:
  131. return (icon: Icons.help_outline, color: Colors.grey);
  132. }
  133. }
  134. String? get _displayText {
  135. if (_isReturned) return returnedText;
  136. if (_isClosed) return closedText;
  137. if (_isTransferred) return transferredText;
  138. return _approvalText;
  139. }
  140. // ── 构建 ──
  141. /// 仅状态标签(用于 FormSection trailing)
  142. Widget buildStatusTag(BuildContext context) {
  143. final style = _statusStyle();
  144. final text = _displayText;
  145. if (text == null || text.isEmpty) return const SizedBox.shrink();
  146. final tag = Container(
  147. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
  148. decoration: BoxDecoration(
  149. color: style.color.withValues(alpha: 0.1),
  150. borderRadius: BorderRadius.circular(6),
  151. border: Border.all(
  152. color: style.color.withValues(alpha: 0.3),
  153. width: 0.5,
  154. ),
  155. ),
  156. child: Row(
  157. mainAxisSize: MainAxisSize.min,
  158. children: [
  159. Icon(style.icon, size: 18, color: style.color),
  160. const SizedBox(width: 4),
  161. Text(
  162. text,
  163. style: TextStyle(
  164. fontSize: 13,
  165. fontWeight: FontWeight.w600,
  166. color: style.color,
  167. ),
  168. ),
  169. ],
  170. ),
  171. );
  172. if (onTapStatusTag != null) {
  173. return GestureDetector(onTap: onTapStatusTag, child: tag);
  174. }
  175. return tag;
  176. }
  177. /// 底部操作按钮区
  178. Widget buildActions(BuildContext context) {
  179. if (!_showButtons) return const SizedBox.shrink();
  180. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  181. final l10n = AppLocalizations.of(context);
  182. final bottomPadding = MediaQuery.of(context).padding.bottom;
  183. return Container(
  184. padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomPadding),
  185. decoration: BoxDecoration(
  186. color: colors.bgCard,
  187. border: Border(top: BorderSide(color: colors.border)),
  188. ),
  189. child: Row(
  190. children: [
  191. if (showEdit) ...[
  192. Expanded(
  193. child: TDButton(
  194. icon: TDIcons.edit,
  195. text: _editLabel(l10n),
  196. size: TDButtonSize.medium,
  197. type: TDButtonType.outline,
  198. shape: TDButtonShape.round,
  199. theme: TDButtonTheme.primary,
  200. onTap: onEdit,
  201. ),
  202. ),
  203. ],
  204. if (showEdit && (showSubmit || showCancelSubmit))
  205. const SizedBox(width: 12),
  206. if (showSubmit)
  207. Expanded(
  208. child: TDButton(
  209. icon: TDIcons.send,
  210. text: _submitLabel(l10n),
  211. size: TDButtonSize.medium,
  212. type: TDButtonType.fill,
  213. shape: TDButtonShape.round,
  214. theme: TDButtonTheme.primary,
  215. onTap: onSubmit,
  216. ),
  217. ),
  218. if (showCancelSubmit)
  219. Expanded(
  220. child: TDButton(
  221. icon: TDIcons.rollback,
  222. text: _cancelSubmitLabel(l10n),
  223. size: TDButtonSize.medium,
  224. type: TDButtonType.fill,
  225. shape: TDButtonShape.round,
  226. theme: TDButtonTheme.danger,
  227. onTap: onCancelSubmit,
  228. ),
  229. ),
  230. if (showReturn) ...[
  231. if (showEdit || showSubmit || showCancelSubmit)
  232. const SizedBox(width: 12),
  233. Expanded(
  234. child: TDButton(
  235. icon: Icons.directions_car,
  236. text: _returnLabel(l10n),
  237. size: TDButtonSize.medium,
  238. type: TDButtonType.fill,
  239. shape: TDButtonShape.round,
  240. theme: TDButtonTheme.primary,
  241. onTap: onReturn,
  242. ),
  243. ),
  244. ],
  245. if (showEditReturn) ...[
  246. if (showEdit || showSubmit || showCancelSubmit || showReturn)
  247. const SizedBox(width: 12),
  248. Expanded(
  249. child: TDButton(
  250. icon: Icons.assignment_return_outlined,
  251. text: _editReturnLabel(l10n),
  252. size: TDButtonSize.medium,
  253. type: TDButtonType.outline,
  254. shape: TDButtonShape.round,
  255. theme: TDButtonTheme.primary,
  256. onTap: onEditReturn,
  257. ),
  258. ),
  259. ],
  260. ],
  261. ),
  262. );
  263. }
  264. // ── 按钮标签 ──
  265. /// 默认返回中文按钮标签(不依赖 i18n,简化跨模块复用)
  266. String _editLabel(AppLocalizations l10n) => l10n.get('modify');
  267. String _submitLabel(AppLocalizations l10n) => l10n.get('submit');
  268. String _cancelSubmitLabel(AppLocalizations l10n) => l10n.get('cancelSubmit');
  269. String _returnLabel(AppLocalizations l10n) => l10n.get('returnCar');
  270. String _editReturnLabel(AppLocalizations l10n) => l10n.get('editReturnInfo');
  271. }