card_item.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. class CardItemMenuWidget extends StatelessWidget {
  117. /// 标题文本
  118. final String title;
  119. /// 左侧icon
  120. final Widget? leading;
  121. final String? hint;
  122. /// 右侧组件对齐方式
  123. final AlignmentGeometry rightAlign;
  124. final bool showTopLine;
  125. final bool bottomLine;
  126. final Color? backgroundColor;
  127. final Color? menuColor;
  128. final ValueNotifier<List<CardMenuData>> listNotifier;
  129. final ValueNotifier<CardMenuData?> selNotifier;
  130. final void Function(ValueNotifier<CardMenuData?>, CardMenuData)? onSelectTap;
  131. late MenuController _ctrl;
  132. CardItemMenuWidget(
  133. this.title,
  134. this.listNotifier,
  135. this.selNotifier, {
  136. Key? key,
  137. this.leading,
  138. this.hint,
  139. this.rightAlign = Alignment.centerRight,
  140. this.showTopLine = false,
  141. this.bottomLine = false,
  142. this.backgroundColor = Colors.white,
  143. this.menuColor = Colors.white,
  144. this.onSelectTap,
  145. }) : super(key: key);
  146. void clickMenu() {
  147. if (listNotifier.value.isEmpty) {
  148. MyNavigator.showToast('没有选项数据');
  149. return;
  150. }
  151. if (_ctrl.isOpen) {
  152. _ctrl.close();
  153. } else {
  154. _ctrl.open();
  155. }
  156. }
  157. @override
  158. Widget build(BuildContext context) {
  159. return CardItemWidget(
  160. title,
  161. leading: leading,
  162. rightAlign: rightAlign,
  163. showTopLine: showTopLine,
  164. bottomLine: bottomLine,
  165. backgroundColor: backgroundColor,
  166. rightChild: buildMenu(),
  167. );
  168. }
  169. Widget buildMenu() {
  170. return listNotifier.builder(
  171. (list) => selNotifier.builder(
  172. (selData) {
  173. return MenuAnchor(
  174. style: MenuStyle(
  175. padding: const MaterialStatePropertyAll(EdgeInsets.zero),
  176. alignment: Alignment.bottomRight,
  177. surfaceTintColor: MaterialStatePropertyAll(menuColor),
  178. elevation: const MaterialStatePropertyAll(16),
  179. shape: const MaterialStatePropertyAll(RoundedRectangleBorder(
  180. borderRadius: BorderRadius.all(Radius.circular(16)),
  181. ))),
  182. builder: (_, ctrl, __) => buildMenuLabel(ctrl, selData),
  183. menuChildren: List.generate(list.length, (index) {
  184. return buildMenuItem(list[index], selData);
  185. }).toList(),
  186. );
  187. },
  188. ),
  189. );
  190. }
  191. /// 菜单栏
  192. Widget buildMenuLabel(MenuController ctrl, CardMenuData? selData) {
  193. _ctrl = ctrl;
  194. return GestureDetector(
  195. behavior: HitTestBehavior.opaque,
  196. onTap: clickMenu,
  197. child: Row(
  198. mainAxisSize: MainAxisSize.min,
  199. children: [
  200. Expanded(
  201. child: Text(
  202. selData?.name != null ? selData!.name! : hint ?? '',
  203. textAlign: TextAlign.right,
  204. style: TextStyle(
  205. color: selData?.name != null ? const Color(0xFF01B2C8) : MyColor.c_666666,
  206. fontSize: 14,
  207. fontWeight: FontWeight.w500,
  208. ),
  209. ),
  210. ),
  211. selData == null
  212. ? Padding(
  213. padding: const EdgeInsets.only(left: 8),
  214. child: Image.asset(imgItemArrowDown, width: 20, color: const Color(0xFF01B2C8)),
  215. )
  216. : const SizedBox.shrink(),
  217. ],
  218. ),
  219. );
  220. }
  221. Widget buildMenuItem(CardMenuData item, CardMenuData? selData) {
  222. return MenuItemButton(
  223. onPressed: () {
  224. if (item.value == selData?.value) return;
  225. selNotifier.value = item;
  226. onSelectTap?.call(selNotifier, item);
  227. },
  228. child: Container(
  229. constraints: const BoxConstraints(minWidth: 120),
  230. alignment: Alignment.center,
  231. child: Text(
  232. item.name ?? '',
  233. style: TextStyle(color: item.value == selData?.value ? const Color(0xFF01B2C8) : MyColor.c_666666),
  234. ),
  235. ),
  236. );
  237. }
  238. }
  239. class CardMenuData {
  240. String? name;
  241. dynamic value;
  242. bool select = false;
  243. CardMenuData(this.name, this.value);
  244. @override
  245. bool operator ==(Object other) {
  246. if (other is! CardMenuData) return false;
  247. return name == other.name && value == other.value;
  248. }
  249. }
  250. class CardWidgets {
  251. CardWidgets._();
  252. static Widget buildEdit(
  253. bool isDetail,
  254. String title,
  255. String? value, {
  256. String? hint = '点击填写',
  257. TextInputType? inputType,
  258. ValueChanged<String>? onChanged,
  259. List<TextInputFormatter>? formatters,
  260. Color? backgroundColor = Colors.white,
  261. bool bottomLine = true,
  262. }) {
  263. if (isDetail) {
  264. return CardItemWidget(
  265. title,
  266. rightText: value,
  267. bottomLine: bottomLine,
  268. backgroundColor: backgroundColor,
  269. );
  270. }
  271. return CardItemWidget(
  272. title,
  273. rightChild: TextField(
  274. controller: TextEditingController(text: value),
  275. keyboardType: inputType,
  276. textAlign: TextAlign.right,
  277. decoration: InputDecoration(
  278. hintText: hint,
  279. hintStyle: const TextStyle(color: MyColor.c_666666, fontSize: 14),
  280. border: InputBorder.none,
  281. contentPadding: EdgeInsets.zero,
  282. isDense: true,
  283. ),
  284. style: const TextStyle(color: Color(0xFF01B2C8), fontSize: 14, fontWeight: FontWeight.w500),
  285. textInputAction: TextInputAction.next,
  286. inputFormatters: formatters,
  287. onChanged: onChanged,
  288. ),
  289. backgroundColor: backgroundColor,
  290. bottomLine: bottomLine,
  291. );
  292. }
  293. static Widget buildDate(
  294. bool isDetail,
  295. String title,
  296. ValueNotifier<String?> notifier, {
  297. String? Function(DateTime?)? onResult,
  298. DateTime? firstDate,
  299. DateTime? lastDate,
  300. }) {
  301. return notifier.builder((v) {
  302. if (isDetail) {
  303. return CardItemWidget(title, rightText: v, bottomLine: true);
  304. }
  305. return CardItemWidget(
  306. title,
  307. rightText: v,
  308. trailing: v == null
  309. ? Image.asset(
  310. imgItemArrowDown,
  311. width: 20,
  312. color: const Color(0xFF01B2C8),
  313. )
  314. : const SizedBox.shrink(),
  315. bottomLine: true,
  316. onTap: () async {
  317. var date = await showDatePicker(
  318. context: MyNavigator.navigator.currentState!.context,
  319. firstDate: firstDate ?? DateTime(2020),
  320. lastDate: lastDate ?? DateTime.now(),
  321. );
  322. if (date != null && onResult != null) {
  323. notifier.value = onResult.call(date);
  324. }
  325. },
  326. );
  327. });
  328. }
  329. static Widget buildMenuDialog(
  330. bool isDetail,
  331. String title,
  332. String? value,
  333. List<CardMenuData>? list,
  334. void Function(List<CardMenuData>)? onSelectTap, {
  335. bool multiple = false,
  336. String? hint,
  337. Widget? trailing,
  338. }) {
  339. return CardItemWidget(
  340. title,
  341. rightText: value,
  342. bottomLine: true,
  343. hint: hint,
  344. trailing: isDetail || value != null
  345. ? null
  346. : trailing ??
  347. Image.asset(
  348. imgItemArrowDown,
  349. width: 20,
  350. color: const Color(0xFF01B2C8),
  351. ),
  352. onTap: isDetail
  353. ? null
  354. : () async {
  355. List<CardMenuData>? selectList = await showMenuDialog(
  356. list,
  357. title: title,
  358. multiple: multiple,
  359. selectCountMax: 3,
  360. );
  361. if (selectList == null) return;
  362. onSelectTap?.call(selectList);
  363. },
  364. );
  365. }
  366. static Widget buildMenu(
  367. bool isDetail,
  368. String title,
  369. ValueNotifier<List<CardMenuData>> listNotifier,
  370. ValueNotifier<CardMenuData?> selNotifier,
  371. void Function(ValueNotifier<CardMenuData?>, CardMenuData)? onSelectTap, {
  372. String? hint,
  373. }) {
  374. if (isDetail) {
  375. return selNotifier.builder(
  376. (v) => CardItemWidget(
  377. title,
  378. rightText: v?.name,
  379. bottomLine: true,
  380. hint: hint,
  381. ),
  382. );
  383. }
  384. return CardItemMenuWidget(
  385. title,
  386. listNotifier,
  387. selNotifier,
  388. onSelectTap: onSelectTap,
  389. bottomLine: true,
  390. hint: hint,
  391. );
  392. }
  393. }