picker.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. // ignore_for_file: unnecessary_set_literal
  2. import 'package:flutter/material.dart';
  3. import 'package:wisdom_cli/src/assets.gen.dart';
  4. import 'package:wisdom_cli/wisdom_cli.dart';
  5. ///Picker抽象类
  6. ///[WPicker],
  7. ///[WPickerUtil.infinity],
  8. ///[WPickerUtil.multiple],
  9. ///[WPickerUtil.single]中的[getData]返回的[List.item]类,应该要实现该抽象类
  10. ///
  11. abstract class WPickerEntity {
  12. ///显示的名字
  13. String get selectName;
  14. ///唯一值
  15. Object get selectUnique;
  16. ///是否禁用
  17. bool get selectDisabled;
  18. }
  19. ///
  20. ///通用快捷的构造[WPickerEntity],
  21. ///只需要传入一个[name],
  22. ///[unique]为次要的。不传也可以
  23. class SinglePickerEntity implements WPickerEntity {
  24. final String? name;
  25. final Object? unique;
  26. final bool? disabled;
  27. final List? child;
  28. final List? cascade;
  29. SinglePickerEntity({
  30. this.name,
  31. this.unique,
  32. this.disabled = false,
  33. this.child,
  34. this.cascade,
  35. });
  36. @override
  37. String get selectName => name ?? '';
  38. @override
  39. Object get selectUnique => unique ?? '';
  40. @override
  41. bool get selectDisabled => disabled ?? false;
  42. List? get childList => child ?? [];
  43. List get cascadeList => cascade ?? [];
  44. ///Map快速构造[List<SinglePickerEntity>]
  45. ///
  46. ///```json
  47. ///{
  48. ///unique1:name1,
  49. ///unique2:name2,
  50. ///unique3:name3,
  51. ///unique4:name4,
  52. ///unique5:name5,
  53. ///unique6:name6,
  54. ///....
  55. ///}
  56. ///```
  57. static Future<List<SinglePickerEntity>> map2entity(
  58. Map<dynamic, String> data) async {
  59. final keys = data.keys;
  60. final list = keys
  61. .map((key) => SinglePickerEntity(name: data[key]!, unique: key))
  62. .toList();
  63. return list;
  64. }
  65. ///Map快速构造[List<SinglePickerEntity>]
  66. ///
  67. ///此构造方法[unique]也会使用[name],请确保数组无重复项
  68. ///```json
  69. ///[name1,name2,name3,...]
  70. ///```
  71. static Future<List<SinglePickerEntity>> list2entity(List<String> data) async {
  72. final list = data.map((e) => SinglePickerEntity(name: e)).toList();
  73. return list;
  74. }
  75. ///Map快速构造[List<SinglePickerEntity>]
  76. /// list<dynamic> to entity
  77. /// @data 列表数据
  78. /// @label 标题字段
  79. /// @value 值字段
  80. static Future<List<SinglePickerEntity>> listMap2entity(
  81. List<dynamic> data, label, value,
  82. {List? enableValue}) async {
  83. final list = data
  84. .map(
  85. (e) => SinglePickerEntity(
  86. name: e['$label'],
  87. unique: e['$value'],
  88. disabled: (enableValue != null && enableValue.length > 0)
  89. ? enableValue.contains(e['$value'])
  90. : false,
  91. ),
  92. )
  93. .toList();
  94. return list;
  95. }
  96. }
  97. ///选择组件、非常基础。
  98. ///
  99. ///[getData]异步返回一个[List],
  100. ///列表项[List.item]应该是一个[WPickerEntity]的实现类
  101. ///
  102. ///具体使用可参[底部弹出层封装]
  103. ///[WPickerUtil.single]、
  104. ///[WPickerUtil.multiple]、
  105. ///[WPickerUtil.infinity]
  106. ///
  107. ///
  108. class WPicker<T extends WPickerEntity> extends StatefulWidget {
  109. WPicker({
  110. Key? key,
  111. this.getData,
  112. this.multiple = false,
  113. this.dense = false,
  114. this.initialValue = const [],
  115. this.listData,
  116. // this.enableValue = const [],
  117. this.onChanged,
  118. this.isSearch = false,
  119. }) : assert(multiple != null),
  120. assert(dense != null),
  121. assert(initialValue != null),
  122. assert(initialValue!.length <= 1 || multiple == true),
  123. super(key: key);
  124. ///是否多选
  125. final bool? multiple;
  126. final bool? dense;
  127. /// 是否可搜索
  128. final bool? isSearch;
  129. ///列表项[List.item]应该是一个[WPickerEntity]的实现类
  130. final WSinglePickerGetData<T>? getData;
  131. final List<T>? listData;
  132. ///为了单选多选的通用性,[initialValue]是一个List。
  133. ///当单选时,即[multiple==false],应满足[initialValue.length<=1]
  134. final List<T>? initialValue;
  135. // final List enableValue;
  136. ///选项发生改变时触发。
  137. ///[isTap==true]时,说明触发该函数的是由用户点击引起的,
  138. ///[isTap==false]时,说明触发该函数可能是由初始化,或其他组件内部原因引起的。
  139. final void Function(List<T>? value, bool isTap)? onChanged;
  140. @override
  141. _WPickerState<T> createState() => _WPickerState<T>();
  142. }
  143. class _WPickerState<T extends WPickerEntity> extends State<WPicker<T>> {
  144. bool loading = false;
  145. List<T>? list = [];
  146. List<T>? initList = [];
  147. List<T>? value;
  148. void getData() async {
  149. if (widget.getData != null) {
  150. loading = true;
  151. try {
  152. initList = (await widget.getData!());
  153. list = initList;
  154. } finally {
  155. loading = false;
  156. }
  157. setState(() {});
  158. } else {
  159. loading = true;
  160. try {
  161. initList = widget.listData;
  162. list = initList;
  163. } finally {
  164. loading = false;
  165. }
  166. setState(() {});
  167. }
  168. }
  169. @override
  170. void initState() {
  171. super.initState();
  172. value = List.of(widget.initialValue ?? []);
  173. widget.onChanged!(value, false);
  174. getData();
  175. }
  176. ///是否已经选中?
  177. ///判断唯一值
  178. bool isSelected(T v) {
  179. return value!.indexWhere((e) => e.selectUnique == v.selectUnique) != -1;
  180. }
  181. ///点击事件
  182. ///会触发widget.onChanged(selects, true);
  183. void ontap(T v) {
  184. if (!(v.selectDisabled)) {
  185. if (!widget.multiple!) {
  186. value = [v];
  187. } else {
  188. final exit = isSelected(v);
  189. if (exit) {
  190. value!.removeWhere((e) => e.selectUnique == v.selectUnique);
  191. } else {
  192. value!.add(v);
  193. }
  194. }
  195. setState(() {});
  196. if (widget.onChanged != null) {
  197. widget.onChanged!(value, true);
  198. }
  199. }
  200. }
  201. void onInput(v) {
  202. List cList =
  203. (initList!.map((e) => e.selectName.contains(v) ? e : null)).toList();
  204. List<T> tempList = [];
  205. cList.forEach((element) {
  206. if (element != null) {
  207. tempList.add(element);
  208. }
  209. });
  210. list = tempList;
  211. setState(() {});
  212. }
  213. /// 为解决flutter截断不完整
  214. String breakWord(String text) {
  215. if (text.isEmpty) {
  216. return text;
  217. }
  218. String breakWord = ' ';
  219. text.runes.forEach((element) {
  220. breakWord += String.fromCharCode(element);
  221. breakWord += '\u200B';
  222. });
  223. return breakWord;
  224. }
  225. @override
  226. Widget build(BuildContext context) {
  227. if (loading) return Center(child: WLoading());
  228. final colorScheme = WTheme.of(context).colorScheme;
  229. return Stack(
  230. children: [
  231. if (list == null || list!.length == 0 || list?.isEmpty == true) ...[
  232. WEmptyWidget(
  233. margin: EdgeInsets.only(
  234. top: widget.isSearch == true ? 59.pt : 15.pt,
  235. ),
  236. text: WisText('暂无数据'),
  237. ),
  238. ] else ...[
  239. ListView(
  240. padding: EdgeInsets.only(
  241. top: widget.isSearch == true ? 49.pt : 0,
  242. ),
  243. children: list!.map(
  244. (value) {
  245. return Wisdom(
  246. width: MediaQuery.of(context).size.width,
  247. margin: EdgeInsets.symmetric(horizontal: 15.pt),
  248. border: Border(bottom: Divider.createBorderSide(context)),
  249. child: ListTile(
  250. onTap: () => ontap(value),
  251. dense: widget.dense,
  252. contentPadding: EdgeInsets.zero,
  253. selected: isSelected(value),
  254. enabled: !(value.selectDisabled),
  255. title: Wisdom.row(
  256. width: MediaQuery.of(context).size.width,
  257. // mainAxisAlignment: MainAxisAlignment.spaceBetween,
  258. children: [
  259. // 增加勾选按钮样式
  260. if (isSelected(value)) ...[
  261. Wisdom(
  262. padding: EdgeInsets.only(right: 10.pt),
  263. child: widget.multiple == true
  264. ? Image(
  265. image: AssetList.$fxxz_png_image,
  266. height: 16.pt)
  267. : Image(
  268. image: AssetList.$danxuan_png_image,
  269. height: 16.pt),
  270. ),
  271. WisText(
  272. '${breakWord(value.selectName)}',
  273. color: colorScheme.primary,
  274. maxLines: 1,
  275. overflow: TextOverflow.ellipsis,
  276. ).asExpanded(),
  277. ] else ...[
  278. Wisdom(
  279. padding: EdgeInsets.only(right: 10.pt),
  280. child: widget.multiple == true
  281. ? Image(
  282. image: AssetList.$dx_png_image,
  283. height: 16.pt)
  284. : Image(
  285. image: AssetList.$danxuanyuan_png_image,
  286. height: 16.pt),
  287. ),
  288. WisText(
  289. '${breakWord(value.selectName)}',
  290. maxLines: 1,
  291. overflow: TextOverflow.ellipsis,
  292. ).asExpanded(),
  293. ],
  294. ],
  295. ),
  296. ),
  297. );
  298. },
  299. ).toList(),
  300. ),
  301. ],
  302. if (widget.isSearch == true) ...[
  303. Positioned(
  304. top: 0,
  305. left: 0,
  306. right: 0,
  307. child: WSearch(
  308. hintText: '输入关键词搜索',
  309. onSubmitted: (v) => onInput(v),
  310. onCancel: () => onInput(''),
  311. onChanged: (v) => onInput(v),
  312. moonlight: false,
  313. padding: EdgeInsets.symmetric(vertical: 10.pt),
  314. isDark: true,
  315. backColor: Colors.white,
  316. ),
  317. ),
  318. ],
  319. ],
  320. );
  321. }
  322. }
  323. class WTreePicker<T extends WPickerEntity> extends StatefulWidget {
  324. WTreePicker({
  325. Key? key,
  326. required this.data,
  327. required this.labelname,
  328. required this.valuename,
  329. required this.childname,
  330. required this.firstName,
  331. required this.firstValue,
  332. this.multiple = false,
  333. this.dense = false,
  334. this.initialValue = const [],
  335. // this.enableValue = const [],
  336. this.onChanged,
  337. this.isSearch = false,
  338. }) : super(key: key);
  339. final String? labelname;
  340. final String? valuename;
  341. final String? childname;
  342. final String? firstName;
  343. final String? firstValue;
  344. ///是否多选
  345. final bool? multiple;
  346. final bool? dense;
  347. /// 是否可搜索
  348. final bool? isSearch;
  349. ///列表项[List.item]应该是一个[WPickerEntity]的实现类
  350. final List? data;
  351. ///为了单选多选的通用性,[initialValue]是一个List。
  352. ///当单选时,即[multiple==false],应满足[initialValue.length<=1]
  353. final List? initialValue;
  354. // final List enableValue;
  355. ///选项发生改变时触发。
  356. ///[isTap==true]时,说明触发该函数的是由用户点击引起的,
  357. ///[isTap==false]时,说明触发该函数可能是由初始化,或其他组件内部原因引起的。
  358. final void Function(List value, bool isTap)? onChanged;
  359. @override
  360. _WTreePickerState<T> createState() => _WTreePickerState<T>();
  361. }
  362. class _WTreePickerState<T extends WPickerEntity> extends State<WTreePicker<T>> {
  363. bool? loading = false;
  364. List? list = [];
  365. var _fistValue;
  366. List? initList = [];
  367. List? secondList = [];
  368. String? _searchValue;
  369. List? value;
  370. void getData() async {
  371. if (widget.data != null) {
  372. loading = true;
  373. try {
  374. initList = widget.data;
  375. list = initList!;
  376. _fistValue = initList!.length > 0 ? initList![0] : null;
  377. secondList = _fistValue[widget.childname] ?? [];
  378. } finally {
  379. loading = false;
  380. }
  381. setState(() {});
  382. }
  383. }
  384. @override
  385. void initState() {
  386. super.initState();
  387. value = List.of(widget.initialValue ?? []);
  388. getData();
  389. }
  390. ///是否已经选中?
  391. ///判断唯一值
  392. bool isSelected(v) {
  393. return value!
  394. .indexWhere((e) => e[widget.valuename] == v[widget.valuename]) !=
  395. -1;
  396. }
  397. ///点击事件
  398. ///会触发widget.onChanged(selects, true);
  399. void ontap(v) {
  400. // if (!(v?.selectDisabled ?? false)) {
  401. if (widget.multiple == false) {
  402. value = [v];
  403. } else {
  404. final exit = isSelected(v);
  405. if (exit) {
  406. value!.removeWhere((e) => e[widget.valuename] == v[widget.valuename]);
  407. } else {
  408. value!.add(v);
  409. }
  410. }
  411. setState(() {});
  412. if (widget.onChanged != null) {
  413. widget.onChanged!(value!, true);
  414. }
  415. }
  416. onSelectFirst(v) {
  417. _fistValue = v;
  418. secondList = _fistValue[widget.childname] ?? [];
  419. setState(() {});
  420. }
  421. void onInput(v) {
  422. _searchValue = v;
  423. if (v == null || v == '') {
  424. list = initList!;
  425. } else {
  426. List tempList = [];
  427. initList!.forEach((ele) => {
  428. ele[widget.childname].forEach((item) => {
  429. if (item[widget.labelname].contains(v))
  430. {
  431. tempList.add(item),
  432. }
  433. }),
  434. });
  435. list = tempList;
  436. }
  437. setState(() {});
  438. }
  439. @override
  440. Widget build(BuildContext context) {
  441. final colorScheme = WTheme.of(context).colorScheme;
  442. if (loading == true) return Center(child: WLoading());
  443. return Stack(
  444. children: [
  445. Wisdom.row(
  446. color: Colors.white,
  447. mainAxisSize: MainAxisSize.min,
  448. children: [
  449. if (_searchValue != null && _searchValue != '') ...[
  450. if (list == null || list!.length == 0) ...[
  451. Wisdom(
  452. width: MediaQuery.of(context).size.width,
  453. padding: EdgeInsets.only(
  454. top: widget.isSearch == true ? 49.pt : 0,
  455. ),
  456. child: Center(
  457. child: WEmptyWidget(
  458. text: WisText('搜索不到,空空如也~'),
  459. ),
  460. ),
  461. ),
  462. ] else ...[
  463. SizedBox(
  464. height: double.infinity,
  465. width: MediaQuery.of(context).size.width,
  466. child: ListView(
  467. padding: EdgeInsets.only(
  468. top: widget.isSearch == true ? 49.pt : 0,
  469. ),
  470. children: list!.map(
  471. (value) {
  472. return Wisdom(
  473. margin: EdgeInsets.symmetric(horizontal: 15.pt),
  474. border:
  475. Border(bottom: Divider.createBorderSide(context)),
  476. child: ListTile(
  477. onTap: () => ontap(value),
  478. dense: widget.dense,
  479. contentPadding: EdgeInsets.zero,
  480. selected: false,
  481. enabled: true,
  482. title: Row(
  483. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  484. children: [
  485. WisText(
  486. '${value[widget.labelname] ?? '--'}',
  487. maxLines: 1,
  488. overflow: TextOverflow.ellipsis,
  489. ).asExpanded(),
  490. // 增加勾选按钮样式
  491. if (isSelected(value)) ...[
  492. Wisdom(
  493. padding: EdgeInsets.only(left: 10.pt),
  494. child: Image(
  495. image: AssetList.$xzh_png_image,
  496. height: 10.pt,
  497. ),
  498. ),
  499. ],
  500. ],
  501. ),
  502. ),
  503. );
  504. },
  505. ).toList(),
  506. ),
  507. ).asExpanded(),
  508. ]
  509. ] else ...[
  510. if (list != null && list!.length > 0) ...[
  511. SizedBox(
  512. height: double.infinity,
  513. width: 130.pt,
  514. child: ListView(
  515. padding: EdgeInsets.only(
  516. top: widget.isSearch == true ? 49.pt : 0,
  517. ),
  518. children: list!.map(
  519. (value) {
  520. return Wisdom(
  521. margin: EdgeInsets.symmetric(horizontal: 15.pt),
  522. border:
  523. Border(bottom: Divider.createBorderSide(context)),
  524. child: ListTile(
  525. onTap: () => onSelectFirst(value),
  526. dense: widget.dense,
  527. contentPadding: EdgeInsets.zero,
  528. selected: _fistValue != null &&
  529. _fistValue[widget.firstValue] ==
  530. value[widget.firstValue],
  531. enabled: true,
  532. title: Row(
  533. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  534. children: [
  535. if (_fistValue != null &&
  536. _fistValue[widget.firstValue] ==
  537. value[widget.firstValue]) ...[
  538. Wisdom(
  539. color: colorScheme.primary,
  540. height: 14.pt,
  541. width: 2.pt,
  542. margin: EdgeInsets.only(right: 5.pt),
  543. ),
  544. ],
  545. WisText(
  546. '${value[widget.firstName] ?? '--'}',
  547. maxLines: 1,
  548. color: _fistValue != null &&
  549. _fistValue[widget.firstValue] ==
  550. value[widget.firstValue]
  551. ? colorScheme.primary
  552. : null,
  553. overflow: TextOverflow.ellipsis,
  554. ).asExpanded(),
  555. ],
  556. ),
  557. ),
  558. );
  559. },
  560. ).toList(),
  561. ),
  562. ),
  563. if (secondList != null && secondList!.length > 0) ...[
  564. SizedBox(
  565. height: double.infinity,
  566. width: MediaQuery.of(context).size.width - 130.pt,
  567. child: ListView(
  568. padding: EdgeInsets.only(
  569. top: widget.isSearch == true ? 49.pt : 0,
  570. ),
  571. children: secondList!.map(
  572. (value) {
  573. return Wisdom(
  574. margin: EdgeInsets.only(right: 15.pt),
  575. border: Border(
  576. bottom: Divider.createBorderSide(context)),
  577. child: ListTile(
  578. onTap: () => ontap(value),
  579. dense: widget.dense,
  580. contentPadding: EdgeInsets.zero,
  581. selected: false,
  582. enabled: true,
  583. title: Row(
  584. mainAxisAlignment:
  585. MainAxisAlignment.spaceBetween,
  586. children: [
  587. WisText(
  588. '${value[widget.labelname] ?? '--'}',
  589. maxLines: 1,
  590. overflow: TextOverflow.ellipsis,
  591. ).asExpanded(),
  592. // 增加勾选按钮样式
  593. if (isSelected(value)) ...[
  594. Wisdom(
  595. padding: EdgeInsets.only(left: 10.pt),
  596. child: Image(
  597. image: AssetList.$xzh_png_image,
  598. height: 10.pt,
  599. ),
  600. ),
  601. ],
  602. ],
  603. ),
  604. ),
  605. );
  606. },
  607. ).toList(),
  608. ),
  609. )
  610. ] else ...[
  611. Wisdom(
  612. width: MediaQuery.of(context).size.width - 130.pt,
  613. padding: EdgeInsets.only(
  614. top: widget.isSearch == true ? 49.pt : 0,
  615. ),
  616. child: Center(
  617. child: WEmptyWidget(
  618. text: WisText('暂无数据'),
  619. ),
  620. ),
  621. ),
  622. ]
  623. ] else ...[
  624. Wisdom(
  625. width: MediaQuery.of(context).size.width,
  626. padding: EdgeInsets.only(
  627. top: widget.isSearch == true ? 49.pt : 0,
  628. ),
  629. child: Center(
  630. child: WEmptyWidget(
  631. text: WisText('暂无数据'),
  632. ),
  633. ),
  634. ),
  635. ]
  636. ]
  637. ],
  638. ),
  639. if (widget.isSearch == true) ...[
  640. Positioned(
  641. top: 0,
  642. left: 0,
  643. right: 0,
  644. child: WSearch(
  645. onCancel: () => onInput(null),
  646. hintText: '输入关键词搜索',
  647. onSubmitted: (v) => onInput(v),
  648. onChanged: (v) => onInput(v),
  649. moonlight: false,
  650. padding: EdgeInsets.symmetric(vertical: 10.pt),
  651. isDark: true,
  652. backColor: Colors.white,
  653. ),
  654. ),
  655. ],
  656. ],
  657. );
  658. }
  659. }