app_input_dialog.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:tdesign_flutter/tdesign_flutter.dart';
  4. import '../../core/i18n/app_localizations.dart';
  5. import '../../core/theme/app_colors_extension.dart';
  6. enum AppInputType { text, integer, decimal }
  7. class AppInputDialog {
  8. static Future<String?> show({
  9. required BuildContext context,
  10. required String title,
  11. String? hintText,
  12. String initialText = '',
  13. AppInputType inputType = AppInputType.text,
  14. int decimalPlaces = 2,
  15. num? min,
  16. num? max,
  17. bool barrierDismissible = true,
  18. }) {
  19. final l10n = AppLocalizations.of(context);
  20. final ctrl = TextEditingController(text: initialText);
  21. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  22. TextInputType keyboardType;
  23. List<TextInputFormatter>? inputFormatters;
  24. switch (inputType) {
  25. case AppInputType.text:
  26. keyboardType = TextInputType.text;
  27. inputFormatters = null;
  28. case AppInputType.integer:
  29. keyboardType = const TextInputType.numberWithOptions(decimal: false);
  30. inputFormatters = [FilteringTextInputFormatter.digitsOnly];
  31. case AppInputType.decimal:
  32. keyboardType = const TextInputType.numberWithOptions(decimal: true);
  33. inputFormatters = [
  34. FilteringTextInputFormatter.allow(
  35. RegExp(r'^-?\d*\.?\d{0,' + decimalPlaces.toString() + '}'),
  36. ),
  37. ];
  38. }
  39. void onConfirm() {
  40. final value = ctrl.text.trim();
  41. if (value.isEmpty) {
  42. Navigator.pop(context);
  43. return;
  44. }
  45. if (inputType != AppInputType.text) {
  46. final num? parsed = double.tryParse(value);
  47. if (parsed == null) {
  48. TDToast.showText(l10n.get('enterValidNumber'), context: context);
  49. return;
  50. }
  51. if (min != null && parsed < min) {
  52. TDToast.showText('${l10n.get('minValue')}: $min', context: context);
  53. return;
  54. }
  55. if (max != null && parsed > max) {
  56. TDToast.showText('${l10n.get('maxValue')}: $max', context: context);
  57. return;
  58. }
  59. }
  60. Navigator.pop(context, value);
  61. }
  62. Widget buildCard(Color _, BuildContext ctx) {
  63. final bottomInset = MediaQuery.of(ctx).viewInsets.bottom;
  64. return Material(
  65. color: Colors.transparent,
  66. child: GestureDetector(
  67. onTap: () => FocusScope.of(ctx).unfocus(),
  68. child: Padding(
  69. padding: EdgeInsets.only(bottom: bottomInset),
  70. child: Center(
  71. child: GestureDetector(
  72. onTap: () {},
  73. child: Container(
  74. margin: const EdgeInsets.symmetric(horizontal: 32),
  75. padding: const EdgeInsets.fromLTRB(20, 24, 20, 8),
  76. decoration: BoxDecoration(
  77. color: colors.bgCard,
  78. borderRadius: BorderRadius.circular(12),
  79. ),
  80. child: Column(
  81. mainAxisSize: MainAxisSize.min,
  82. children: [
  83. Text(title,
  84. style: TextStyle(
  85. fontSize: 18,
  86. fontWeight: FontWeight.w600,
  87. color: colors.textPrimary)),
  88. const SizedBox(height: 20),
  89. _InputField(
  90. controller: ctrl,
  91. keyboardType: keyboardType,
  92. inputFormatters: inputFormatters,
  93. hintText: hintText ?? l10n.get('pleaseEnter'),
  94. ),
  95. if (inputType != AppInputType.text &&
  96. (min != null || max != null)) ...[
  97. const SizedBox(height: 8),
  98. Align(
  99. alignment: Alignment.centerRight,
  100. child: Text(_rangeHint(min, max),
  101. style: TextStyle(
  102. fontSize: 12, color: colors.textPlaceholder)),
  103. ),
  104. ],
  105. const SizedBox(height: 20),
  106. Row(
  107. children: [
  108. Expanded(
  109. child: TextButton(
  110. onPressed: () => Navigator.pop(ctx),
  111. child: Text(l10n.get('cancel'),
  112. style: TextStyle(
  113. fontSize: 16,
  114. color: colors.textSecondary)),
  115. ),
  116. ),
  117. const SizedBox(width: 12),
  118. Expanded(
  119. child: TextButton(
  120. onPressed: onConfirm,
  121. child: Text(l10n.get('confirm'),
  122. style: TextStyle(
  123. fontSize: 16,
  124. fontWeight: FontWeight.w600,
  125. color: colors.primary)),
  126. ),
  127. ),
  128. ],
  129. ),
  130. ],
  131. ),
  132. ),
  133. ),
  134. ),
  135. ),
  136. ),
  137. );
  138. }
  139. return showGeneralDialog<String>(
  140. context: context,
  141. barrierDismissible: barrierDismissible,
  142. barrierLabel: barrierDismissible ? 'Dismiss' : null,
  143. barrierColor: Colors.black38,
  144. transitionBuilder: (ctx, animation, secondaryAnimation, child) {
  145. return FadeTransition(
  146. opacity:
  147. CurvedAnimation(parent: animation, curve: Curves.easeOut),
  148. child: child,
  149. );
  150. },
  151. pageBuilder: (ctx, animation, secondaryAnimation) {
  152. return buildCard(Colors.transparent, ctx);
  153. },
  154. );
  155. }
  156. static String _rangeHint(num? min, num? max) {
  157. if (min != null && max != null) return '$min ~ $max';
  158. if (min != null) return '≥ $min';
  159. if (max != null) return '≤ $max';
  160. return '';
  161. }
  162. }
  163. class _InputField extends StatefulWidget {
  164. final TextEditingController controller;
  165. final TextInputType keyboardType;
  166. final List<TextInputFormatter>? inputFormatters;
  167. final String hintText;
  168. const _InputField({
  169. required this.controller,
  170. required this.keyboardType,
  171. this.inputFormatters,
  172. required this.hintText,
  173. });
  174. @override
  175. State<_InputField> createState() => _InputFieldState();
  176. }
  177. class _InputFieldState extends State<_InputField> {
  178. bool _hasText = false;
  179. @override
  180. void initState() {
  181. super.initState();
  182. _hasText = widget.controller.text.isNotEmpty;
  183. widget.controller.addListener(_onChange);
  184. }
  185. void _onChange() {
  186. final has = widget.controller.text.isNotEmpty;
  187. if (has != _hasText) setState(() => _hasText = has);
  188. }
  189. @override
  190. void dispose() {
  191. widget.controller.removeListener(_onChange);
  192. super.dispose();
  193. }
  194. @override
  195. Widget build(BuildContext context) {
  196. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  197. return Container(
  198. decoration: BoxDecoration(
  199. color: colors.bgPage,
  200. borderRadius: BorderRadius.circular(4),
  201. ),
  202. child: TextField(
  203. controller: widget.controller,
  204. autofocus: true,
  205. keyboardType: widget.keyboardType,
  206. inputFormatters: widget.inputFormatters,
  207. style: TextStyle(fontSize: 16, color: colors.textPrimary),
  208. decoration: InputDecoration(
  209. hintText: widget.hintText,
  210. hintStyle: TextStyle(fontSize: 16, color: colors.textPlaceholder),
  211. contentPadding:
  212. const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
  213. border: InputBorder.none,
  214. isCollapsed: true,
  215. suffixIcon: _hasText
  216. ? GestureDetector(
  217. onTap: () {
  218. widget.controller.clear();
  219. setState(() => _hasText = false);
  220. },
  221. child: const Padding(
  222. padding: EdgeInsets.only(right: 8),
  223. child:
  224. Icon(Icons.close, size: 18, color: Color(0xFFBBBBBB)),
  225. ),
  226. )
  227. : null,
  228. ),
  229. ),
  230. );
  231. }
  232. }