card_item.dart 11 KB

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