card_item.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:lszlgl/config/colors.dart';
  4. import 'package:lszlgl/ext/value_notifier_ext.dart';
  5. import '../router/my_navigator.dart';
  6. import 'menu_dialog.dart';
  7. class CardItemWidget extends StatelessWidget {
  8. /// 标题文本
  9. final String title;
  10. /// 左侧icon
  11. final Widget? leading;
  12. /// 右侧icon
  13. final Widget? trailing;
  14. /// 右侧文本
  15. final String? rightText;
  16. final String? hint;
  17. /// 自定义右侧组件
  18. final Widget? rightChild;
  19. /// 右侧组件对齐方式
  20. final AlignmentGeometry rightAlign;
  21. final bool showTopLine;
  22. final bool bottomLine;
  23. /// 点击事件
  24. final VoidCallback? onTap;
  25. final Color? backgroundColor;
  26. // 左右两边 填充比例
  27. final int leftRatio;
  28. final int rightRatio;
  29. const CardItemWidget(
  30. this.title, {
  31. Key? key,
  32. this.leading,
  33. this.trailing,
  34. this.rightText,
  35. this.hint,
  36. this.rightChild,
  37. this.onTap,
  38. this.rightAlign = Alignment.centerRight,
  39. this.showTopLine = false,
  40. this.bottomLine = false,
  41. this.backgroundColor = Colors.white,
  42. this.leftRatio = 1,
  43. this.rightRatio = 1,
  44. }) : super(key: key);
  45. @override
  46. Widget build(BuildContext context) {
  47. return GestureDetector(
  48. onTap: onTap,
  49. behavior: HitTestBehavior.opaque,
  50. child: Column(
  51. mainAxisSize: MainAxisSize.min,
  52. children: [
  53. showTopLine
  54. ? const Divider(color: Color(0xFFF3F3F3), height: 0, thickness: 1)
  55. : const SizedBox.shrink(),
  56. Container(
  57. constraints: const BoxConstraints(minHeight: 48),
  58. padding: const EdgeInsets.all(12),
  59. color: backgroundColor,
  60. child: Row(
  61. children: [
  62. buildLeading(),
  63. buildTitle(),
  64. const SizedBox(width: 4),
  65. buildRight(),
  66. buildTrailing(),
  67. ],
  68. ),
  69. ),
  70. bottomLine
  71. ? const Divider(color: Color(0xFFF3F3F3), height: 0, thickness: 1)
  72. : const SizedBox.shrink(),
  73. ],
  74. ),
  75. );
  76. }
  77. Widget buildLeading() {
  78. if (leading == null) return const SizedBox.shrink();
  79. return Padding(
  80. padding: const EdgeInsets.only(right: 8),
  81. child: leading,
  82. );
  83. }
  84. Widget buildTrailing() {
  85. if (trailing == null) return const SizedBox.shrink();
  86. return Padding(
  87. padding: const EdgeInsets.only(left: 8),
  88. child: trailing,
  89. );
  90. }
  91. Widget buildTitle() {
  92. return Expanded(
  93. flex: leftRatio,
  94. child: Text(
  95. title,
  96. style: const TextStyle(
  97. color: Color(0xFF515151),
  98. fontSize: 14,
  99. fontWeight: FontWeight.w300,
  100. ),
  101. ),
  102. );
  103. }
  104. Widget buildRight() {
  105. Widget child;
  106. if (rightChild != null) {
  107. child = rightChild!;
  108. } else {
  109. child = Text(
  110. (rightText?.isNotEmpty ?? false) ? rightText! : hint ?? '',
  111. textAlign: TextAlign.right,
  112. style: TextStyle(
  113. color: (rightText?.isNotEmpty ?? false) ? const Color(0xFF01B2C8) : MyColor.c_666666,
  114. fontSize: 14,
  115. fontWeight: FontWeight.w500,
  116. ),
  117. );
  118. }
  119. return Expanded(
  120. flex: rightRatio,
  121. child: Container(
  122. alignment: rightAlign,
  123. child: child,
  124. ),
  125. );
  126. }
  127. }
  128. //ignore: must_be_immutable
  129. class CardItemMenuWidget extends StatelessWidget {
  130. /// 标题文本
  131. final String title;
  132. /// 左侧icon
  133. final Widget? leading;
  134. final String? hint;
  135. /// 右侧组件对齐方式
  136. final AlignmentGeometry rightAlign;
  137. final bool showTopLine;
  138. final bool bottomLine;
  139. final Color? backgroundColor;
  140. final Color? menuColor;
  141. final int leftRatio;
  142. final int rightRatio;
  143. final ValueNotifier<List<CardMenuData>> listNotifier;
  144. final ValueNotifier<CardMenuData?> selNotifier;
  145. final void Function(ValueNotifier<CardMenuData?>, CardMenuData)? onSelectTap;
  146. late MenuController _ctrl;
  147. CardItemMenuWidget(
  148. this.title,
  149. this.listNotifier,
  150. this.selNotifier, {
  151. Key? key,
  152. this.leading,
  153. this.hint,
  154. this.rightAlign = Alignment.centerRight,
  155. this.showTopLine = false,
  156. this.bottomLine = false,
  157. this.backgroundColor = Colors.white,
  158. this.menuColor = Colors.white,
  159. this.onSelectTap,
  160. this.leftRatio = 1,
  161. this.rightRatio = 1,
  162. }) : super(key: key);
  163. void clickMenu() {
  164. if (listNotifier.value.isEmpty) {
  165. MyNavigator.showToast('没有选项数据');
  166. return;
  167. }
  168. if (_ctrl.isOpen) {
  169. _ctrl.close();
  170. } else {
  171. _ctrl.open();
  172. }
  173. }
  174. @override
  175. Widget build(BuildContext context) {
  176. return CardItemWidget(
  177. title,
  178. leading: leading,
  179. rightAlign: rightAlign,
  180. showTopLine: showTopLine,
  181. bottomLine: bottomLine,
  182. backgroundColor: backgroundColor,
  183. rightChild: buildMenu(),
  184. leftRatio: leftRatio,
  185. rightRatio: rightRatio,
  186. );
  187. }
  188. Widget buildMenu() {
  189. return listNotifier.builder(
  190. (list) => selNotifier.builder(
  191. (selData) {
  192. return MenuAnchor(
  193. style: MenuStyle(
  194. padding: const WidgetStatePropertyAll(EdgeInsets.zero),
  195. alignment: Alignment.bottomRight,
  196. surfaceTintColor: WidgetStatePropertyAll(menuColor),
  197. elevation: const WidgetStatePropertyAll(16),
  198. shape: const WidgetStatePropertyAll(RoundedRectangleBorder(
  199. borderRadius: BorderRadius.all(Radius.circular(16)),
  200. ))),
  201. builder: (_, ctrl, __) => buildMenuLabel(ctrl, selData),
  202. menuChildren: List.generate(list.length, (index) {
  203. return buildMenuItem(list[index], selData);
  204. }).toList(),
  205. );
  206. },
  207. ),
  208. );
  209. }
  210. /// 菜单栏
  211. Widget buildMenuLabel(MenuController ctrl, CardMenuData? selData) {
  212. _ctrl = ctrl;
  213. return GestureDetector(
  214. behavior: HitTestBehavior.opaque,
  215. onTap: clickMenu,
  216. child: Row(
  217. mainAxisSize: MainAxisSize.min,
  218. children: [
  219. Expanded(
  220. child: Text(
  221. selData?.name != null ? selData!.name! : hint ?? '',
  222. textAlign: TextAlign.right,
  223. style: TextStyle(
  224. color: selData?.name != null ? const Color(0xFF01B2C8) : MyColor.c_666666,
  225. fontSize: 14,
  226. fontWeight: FontWeight.w500,
  227. ),
  228. ),
  229. ),
  230. selData == null
  231. ? const Padding(
  232. padding: EdgeInsets.only(left: 4),
  233. child: Icon(
  234. Icons.keyboard_arrow_down,
  235. size: 24,
  236. color: Color(0xFF01B2C8),
  237. ),
  238. )
  239. : const SizedBox.shrink(),
  240. ],
  241. ),
  242. );
  243. }
  244. Widget buildMenuItem(CardMenuData item, CardMenuData? selData) {
  245. return MenuItemButton(
  246. onPressed: () {
  247. if (item.value == selData?.value) return;
  248. selNotifier.value = item;
  249. onSelectTap?.call(selNotifier, item);
  250. },
  251. child: Container(
  252. constraints: const BoxConstraints(minWidth: 120),
  253. alignment: Alignment.center,
  254. child: Text(
  255. item.name ?? '',
  256. style: TextStyle(
  257. color: item.value == selData?.value ? const Color(0xFF01B2C8) : MyColor.c_666666),
  258. ),
  259. ),
  260. );
  261. }
  262. }
  263. class CardMenuData {
  264. String? name;
  265. dynamic value;
  266. bool select = false;
  267. CardMenuData(this.name, this.value);
  268. @override
  269. bool operator ==(Object other) {
  270. if (other is! CardMenuData) return false;
  271. return name == other.name && value == other.value;
  272. }
  273. }
  274. class CardWidgets {
  275. CardWidgets._();
  276. static Widget buildEdit(
  277. bool isDetail,
  278. String title,
  279. String? value, {
  280. String? hint = '点击填写',
  281. TextInputType? inputType,
  282. ValueChanged<String>? onChanged,
  283. List<TextInputFormatter>? formatters,
  284. Color? backgroundColor = Colors.white,
  285. bool bottomLine = true,
  286. bool obscureText = false,
  287. TextEditingController? ctrl,
  288. int? maxLength,
  289. String? errorText,
  290. int leftRatio = 1,
  291. int rightRatio = 1,
  292. }) {
  293. if (isDetail) {
  294. return CardItemWidget(
  295. title,
  296. rightText: value,
  297. bottomLine: bottomLine,
  298. backgroundColor: backgroundColor,
  299. leftRatio: leftRatio,
  300. rightRatio: rightRatio,
  301. );
  302. }
  303. return CardItemWidget(
  304. title,
  305. rightChild: TextFormField(
  306. controller: ctrl,
  307. initialValue: ctrl == null ? value : null,
  308. keyboardType: inputType,
  309. textAlign: TextAlign.right,
  310. decoration: InputDecoration(
  311. hintText: hint,
  312. hintStyle: const TextStyle(color: MyColor.c_666666, fontSize: 14),
  313. border: InputBorder.none,
  314. contentPadding: EdgeInsets.zero,
  315. isDense: true,
  316. errorText: errorText,
  317. ),
  318. style: const TextStyle(color: Color(0xFF01B2C8), fontSize: 14, fontWeight: FontWeight.w500),
  319. textInputAction: TextInputAction.next,
  320. inputFormatters: formatters,
  321. onChanged: onChanged,
  322. obscureText: obscureText,
  323. maxLength: maxLength,
  324. ),
  325. backgroundColor: backgroundColor,
  326. bottomLine: bottomLine,
  327. leftRatio: leftRatio,
  328. rightRatio: rightRatio,
  329. );
  330. }
  331. static Widget buildDate(
  332. bool isDetail,
  333. String title,
  334. ValueNotifier<String?> notifier, {
  335. String? Function(DateTime?)? onResult,
  336. DateTime? firstDate,
  337. DateTime? lastDate,
  338. String? warmString, // 警告语,日期不可选择时 禁止点击并提示
  339. }) {
  340. return notifier.builder((v) {
  341. if (isDetail) {
  342. return CardItemWidget(title, rightText: v, bottomLine: true);
  343. }
  344. return CardItemWidget(
  345. title,
  346. rightText: v,
  347. trailing: v == null
  348. ? const Icon(
  349. Icons.keyboard_arrow_down,
  350. size: 24,
  351. color: Color(0xFF01B2C8),
  352. )
  353. : const SizedBox.shrink(),
  354. bottomLine: true,
  355. onTap: () async {
  356. if(warmString != null){
  357. MyNavigator.showToast(warmString);
  358. return ;
  359. }
  360. var date = await showDatePicker(
  361. context: MyNavigator.navigator.currentState!.context,
  362. firstDate: firstDate ?? DateTime(2020),
  363. lastDate: lastDate ?? DateTime.now(),
  364. );
  365. if (date != null && onResult != null) {
  366. notifier.value = onResult.call(date);
  367. }
  368. },
  369. );
  370. });
  371. }
  372. static Widget buildMenuDialog(
  373. bool isDetail,
  374. String title,
  375. String? value,
  376. List<CardMenuData>? list,
  377. void Function(List<CardMenuData>)? onSelectTap, {
  378. bool multiple = false,
  379. String? hint,
  380. Widget? trailing,
  381. int? selectCountMax,
  382. bool bottomLine = true,
  383. bool showTopLine = false,
  384. }) {
  385. return CardItemWidget(
  386. title,
  387. rightText: value,
  388. bottomLine: bottomLine,
  389. showTopLine: showTopLine,
  390. hint: hint,
  391. trailing: isDetail || value != null
  392. ? null
  393. : trailing ??
  394. const Icon(
  395. Icons.keyboard_arrow_down,
  396. size: 24,
  397. color: Color(0xFF01B2C8),
  398. ),
  399. onTap: isDetail
  400. ? null
  401. : () async {
  402. List<CardMenuData>? selectList = await showMenuDialog(
  403. list,
  404. title: title,
  405. multiple: multiple,
  406. selectCountMax: selectCountMax,
  407. );
  408. if (selectList == null) return;
  409. onSelectTap?.call(selectList);
  410. },
  411. );
  412. }
  413. static Widget buildMenu(
  414. bool isDetail,
  415. String title,
  416. ValueNotifier<List<CardMenuData>> listNotifier,
  417. ValueNotifier<CardMenuData?> selNotifier,
  418. void Function(ValueNotifier<CardMenuData?>, CardMenuData)? onSelectTap, {
  419. String? hint,
  420. bool showTopLine = false,
  421. bool bottomLine = true,
  422. int leftRatio = 1,
  423. int rightRatio = 1,
  424. }) {
  425. if (isDetail) {
  426. return selNotifier.builder(
  427. (v) => CardItemWidget(
  428. title,
  429. rightText: v?.name,
  430. bottomLine: bottomLine,
  431. hint: hint,
  432. showTopLine: showTopLine,
  433. leftRatio: leftRatio,
  434. rightRatio: rightRatio,
  435. ),
  436. );
  437. }
  438. return CardItemMenuWidget(
  439. title,
  440. listNotifier,
  441. selNotifier,
  442. onSelectTap: onSelectTap,
  443. bottomLine: bottomLine,
  444. hint: hint,
  445. showTopLine: showTopLine,
  446. leftRatio: leftRatio,
  447. rightRatio: rightRatio,
  448. );
  449. }
  450. }