table.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import 'package:flutter/material.dart';
  2. import 'package:vitalapp/components/scroll_list.dart';
  3. import 'table_column.dart';
  4. const _cellPadding = EdgeInsets.symmetric(
  5. horizontal: 14,
  6. vertical: 12,
  7. );
  8. const _cellTextStyle = TextStyle(fontSize: 14);
  9. const _headerBgColor = Color.fromRGBO(233, 237, 240, 1);
  10. /// 数据列表
  11. class VitalTable<T> extends StatefulWidget {
  12. VitalTable({
  13. Key? key,
  14. required this.columns,
  15. required this.source,
  16. this.loading = false,
  17. this.autoHeight = true,
  18. this.showSelect = false,
  19. this.selecteds,
  20. this.onAllRowsSelected,
  21. this.onRowSelected,
  22. this.onRowTap,
  23. this.headerDecoration,
  24. this.rowDecoration,
  25. this.selectedDecoration,
  26. this.headerTextStyle,
  27. this.rowTextStyle,
  28. this.selectedTextStyle,
  29. this.noDataHintText,
  30. this.currectSelected,
  31. this.checkCellWidth = 90,
  32. this.cellPadding = _cellPadding,
  33. }) : assert(() {
  34. if (showSelect == true && selecteds == null) {
  35. throw FlutterError(
  36. "Property `selecteds` must not be null while `selecteds` is not null.");
  37. }
  38. return true;
  39. }()),
  40. super(key: key);
  41. final double checkCellWidth;
  42. final EdgeInsets? cellPadding;
  43. ///无数据提示
  44. final String? noDataHintText;
  45. /// 列配置集合
  46. final List<TableColumn<T>> columns;
  47. /// 数据源
  48. final List<T>? source;
  49. /// 加载中
  50. final bool loading;
  51. /// 自适应高度
  52. final bool autoHeight;
  53. /// 是否显示选择列
  54. final bool showSelect;
  55. /// 已选中行索引
  56. final List<int>? selecteds;
  57. /// 全部勾选事件
  58. ///
  59. /// [value] 选中/取消选中
  60. ///
  61. /// [selectedIndexs] 当前已选所有索引
  62. final void Function(bool value, List<int> selectedIndexs)? onAllRowsSelected;
  63. /// 列勾选事件
  64. ///
  65. /// [value] 选中/取消选中
  66. ///
  67. /// [index] 当前勾选行
  68. ///
  69. /// [selectedIndexs] 当前已选所有索引
  70. final void Function(bool value, int index, List<int> selectedIndexs)?
  71. onRowSelected;
  72. /// 行点击事件
  73. ///
  74. /// [value] 选中/取消选中
  75. ///
  76. /// [index] 当前选中行
  77. ///
  78. /// [selectedIndexs] 当前已选所有索引
  79. final void Function(int index)? onRowTap;
  80. /// 表头装饰器
  81. final BoxDecoration? headerDecoration;
  82. /// 行装饰器
  83. final BoxDecoration? rowDecoration;
  84. /// 选中行装饰器
  85. final BoxDecoration? selectedDecoration;
  86. /// 表头文本样式
  87. final TextStyle? headerTextStyle;
  88. /// 行文本样式
  89. final TextStyle? rowTextStyle;
  90. /// 选中行文本样式
  91. final TextStyle? selectedTextStyle;
  92. /// 当前选中的
  93. int? currectSelected = -1;
  94. @override
  95. State<StatefulWidget> createState() => _VitalTableState<T>();
  96. }
  97. class _VitalTableState<T> extends State<VitalTable<T>> {
  98. final ScrollController _scrollController = ScrollController();
  99. List<int> _selectedIdxs = [];
  100. List<T> _source = [];
  101. late int _currectSelected = widget.currectSelected ?? -1;
  102. @override
  103. void didUpdateWidget(VitalTable<T> oldWidget) {
  104. _loadData();
  105. if (widget.currectSelected != oldWidget.currectSelected) {
  106. _currectSelected = widget.currectSelected ?? -1;
  107. setState(() {});
  108. }
  109. super.didUpdateWidget(oldWidget);
  110. }
  111. @override
  112. void initState() {
  113. _loadData();
  114. super.initState();
  115. }
  116. void _loadData() {
  117. _selectedIdxs = [if (widget.selecteds != null) ...widget.selecteds!];
  118. _source = widget.source ?? [];
  119. }
  120. @override
  121. Widget build(BuildContext context) {
  122. return Column(
  123. mainAxisAlignment: MainAxisAlignment.start,
  124. children: [
  125. // header
  126. if (widget.columns.isNotEmpty) buildHeader(),
  127. // loading
  128. if (widget.loading)
  129. const SizedBox(
  130. height: 5,
  131. child: LinearProgressIndicator(),
  132. ),
  133. // rows
  134. if (widget.autoHeight)
  135. Column(children: createRows())
  136. else
  137. Expanded(
  138. child: AlwaysScrollListView(
  139. scrollController: _scrollController,
  140. child: ListView(
  141. padding: const EdgeInsets.only(right: 10),
  142. controller: _scrollController,
  143. children: createRows(),
  144. ),
  145. ),
  146. ),
  147. const SizedBox(height: 20),
  148. ],
  149. );
  150. }
  151. static Alignment headerAlignSwitch(TextAlign? textAlign) {
  152. switch (textAlign) {
  153. case TextAlign.center:
  154. return Alignment.center;
  155. case TextAlign.left:
  156. return Alignment.centerLeft;
  157. case TextAlign.right:
  158. return Alignment.centerRight;
  159. default:
  160. return Alignment.center;
  161. }
  162. }
  163. Widget buildHeader() {
  164. final decoration = widget.headerDecoration ??
  165. const BoxDecoration(
  166. color: _headerBgColor,
  167. // border: Border(
  168. // bottom: BorderSide(color: Colors.grey.shade300, width: 1),
  169. // ),
  170. );
  171. final cells = <Widget>[];
  172. if (widget.showSelect) {
  173. final checked = _selectedIdxs.isNotEmpty &&
  174. _source.isNotEmpty &&
  175. _selectedIdxs.length == _source.length;
  176. cells.add(
  177. buildCheckCell(
  178. value: checked,
  179. onChanged: (value) {
  180. final checked = value == true;
  181. setState(() {
  182. _selectedIdxs = checked
  183. ? List.generate(_source.length, (index) => index)
  184. : [];
  185. });
  186. widget.onAllRowsSelected?.call(checked, _selectedIdxs);
  187. },
  188. ),
  189. );
  190. }
  191. cells.addAll(
  192. widget.columns.map(
  193. (column) {
  194. final child = column.headerRender != null
  195. ? column.headerRender!()
  196. : Container(
  197. padding: widget.cellPadding,
  198. alignment: headerAlignSwitch(column.textAlign),
  199. child: Wrap(
  200. crossAxisAlignment: WrapCrossAlignment.center,
  201. children: [
  202. Text(
  203. column.headerText!,
  204. textAlign: column.textAlign,
  205. style: widget.headerTextStyle ??
  206. const TextStyle(
  207. fontSize: 14,
  208. fontWeight: FontWeight.bold,
  209. ),
  210. )
  211. ],
  212. ),
  213. );
  214. final cell = buildCellBox(column, child);
  215. return Expanded(flex: column.actualFlex, child: cell);
  216. },
  217. ),
  218. );
  219. return Container(
  220. decoration: decoration,
  221. child: Row(
  222. mainAxisSize: MainAxisSize.min,
  223. children: cells,
  224. ),
  225. );
  226. }
  227. List<Widget> createRows() {
  228. if (_source.isEmpty) {
  229. if (widget.loading) return const [];
  230. var noDataHintText = widget.noDataHintText;
  231. return [
  232. Container(
  233. height: 50,
  234. alignment: Alignment.center,
  235. child: Text(
  236. noDataHintText ?? "No data found",
  237. style: const TextStyle(fontSize: 20),
  238. ),
  239. )
  240. ];
  241. }
  242. final rowDecoration = widget.rowDecoration ??
  243. const BoxDecoration(
  244. // border: Border(
  245. // bottom: BorderSide(color: Colors.grey.shade300, width: 1),
  246. // ),
  247. );
  248. final selectedDecoration = widget.selectedDecoration ?? rowDecoration;
  249. final rows = <Widget>[];
  250. for (var i = 0, len = _source.length; i < len; i++) {
  251. final rowData = _source[i];
  252. rows.add(buildRow(rowData, i, rowDecoration, selectedDecoration));
  253. }
  254. return rows;
  255. }
  256. Widget buildRow(
  257. T rowData,
  258. int index,
  259. BoxDecoration rowDecoration,
  260. BoxDecoration selectedDecoration,
  261. ) {
  262. var isSelected = false;
  263. var decoration = rowDecoration;
  264. final textStyle = widget.rowTextStyle ?? _cellTextStyle;
  265. final selectedTextStyle = widget.selectedTextStyle ?? textStyle;
  266. final cells = <Widget>[];
  267. if (widget.showSelect) {
  268. isSelected = _selectedIdxs.contains(index);
  269. decoration = selectedDecoration;
  270. cells.add(
  271. buildCheckCell(
  272. value: _selectedIdxs.contains(index),
  273. onChanged: (value) {
  274. final checked = value == true;
  275. setState(() {
  276. if (checked) {
  277. if (!_selectedIdxs.contains(index)) {
  278. _selectedIdxs.add(index);
  279. }
  280. } else {
  281. if (_selectedIdxs.contains(index)) {
  282. _selectedIdxs.remove(index);
  283. }
  284. }
  285. });
  286. widget.onRowSelected?.call(checked, index, _selectedIdxs);
  287. },
  288. ),
  289. );
  290. }
  291. cells.addAll(
  292. widget.columns.map(
  293. (column) {
  294. final child = column.render != null
  295. ? column.render!.call(rowData, index)
  296. : Container(
  297. padding: _cellPadding,
  298. alignment: headerAlignSwitch(column.textAlign),
  299. child: Wrap(
  300. crossAxisAlignment: WrapCrossAlignment.center,
  301. children: [
  302. Tooltip(
  303. message: column.textFormatter!.call(rowData, index),
  304. child: Text(
  305. _breakWord(
  306. column.textFormatter!.call(rowData, index)),
  307. textAlign: column.textAlign,
  308. maxLines: 1,
  309. overflow: TextOverflow.ellipsis,
  310. style: isSelected
  311. ? selectedTextStyle.copyWith(
  312. fontSize: column.textFontSize,
  313. )
  314. : textStyle.copyWith(
  315. fontSize: column.textFontSize,
  316. ),
  317. ),
  318. )
  319. ],
  320. ),
  321. // child: Text(
  322. // column.textFormatter!.call(rowData, index),
  323. // textAlign: column.textAlign,
  324. // style: isSelected
  325. // ? widget.selectedTextStyle
  326. // : widget.rowTextStyle,
  327. // ),
  328. );
  329. final cell = buildCellBox(column, child);
  330. return Expanded(flex: column.actualFlex, child: cell);
  331. },
  332. ),
  333. );
  334. return Material(
  335. key: ValueKey("${index}_${rowData.hashCode}"),
  336. child: Ink(
  337. color: _currectSelected == index
  338. ? Theme.of(context).secondaryHeaderColor
  339. : index % 2 == 0
  340. ? Theme.of(context).scaffoldBackgroundColor
  341. : decoration.color ?? Colors.white,
  342. child: InkWell(
  343. mouseCursor: SystemMouseCursors.basic,
  344. onTap: () {
  345. setState(() {});
  346. _currectSelected = index;
  347. widget.onRowTap?.call(index);
  348. },
  349. hoverColor: Theme.of(context).secondaryHeaderColor,
  350. child: Container(
  351. decoration: decoration,
  352. child: Row(
  353. mainAxisSize: MainAxisSize.min,
  354. children: cells,
  355. ),
  356. ),
  357. ),
  358. ),
  359. );
  360. }
  361. Widget buildCellBox(TableColumn<T> column, Widget child) {
  362. if (!column.hasWidthLimit) return child;
  363. return Container(
  364. alignment: Alignment.center,
  365. width: column.width,
  366. constraints: BoxConstraints(
  367. maxWidth: column.maxWidth ?? double.infinity,
  368. minWidth: column.minWidth ?? 0.0,
  369. ),
  370. child: child,
  371. );
  372. }
  373. Widget buildCheckCell({
  374. required bool value,
  375. required ValueChanged<bool> onChanged,
  376. }) {
  377. return Container(
  378. width: widget.checkCellWidth,
  379. alignment: Alignment.center,
  380. child: Checkbox(
  381. activeColor: Theme.of(context).primaryColor,
  382. shape: RoundedRectangleBorder(
  383. borderRadius: BorderRadius.circular(4),
  384. ),
  385. side: const BorderSide(
  386. color: Colors.grey,
  387. width: 1,
  388. ),
  389. value: value,
  390. onChanged: (val) {
  391. onChanged(val == true);
  392. },
  393. ),
  394. );
  395. }
  396. }
  397. /// 插入零宽空格
  398. String _breakWord(String word) {
  399. if (word.isEmpty) return word;
  400. String breakWord = ' ';
  401. for (var element in word.runes) {
  402. breakWord += String.fromCharCode(element);
  403. breakWord += '\u200B';
  404. }
  405. return breakWord;
  406. }