view.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import 'package:fis_jsonrpc/rpc.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:get/get.dart';
  4. import 'package:vnoteapp/architecture/utils/datetime.dart';
  5. import 'package:vnoteapp/components/button.dart';
  6. import 'package:vnoteapp/components/date_picker.dart';
  7. import 'package:vnoteapp/components/panel.dart';
  8. import 'package:vnoteapp/components/search_input.dart';
  9. import 'package:vnoteapp/consts/rpc_enum_labels.dart';
  10. import 'package:vnoteapp/pages/patient/list/widgets/status.dart';
  11. import 'controller.dart';
  12. class PatientListPage extends GetView<PatientListController> {
  13. const PatientListPage({super.key});
  14. @override
  15. Widget build(BuildContext context) {
  16. final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
  17. return Scaffold(
  18. key: scaffoldKey,
  19. endDrawer: _filterdrawer(context),
  20. body: Container(
  21. padding: const EdgeInsets.all(8),
  22. color: Colors.grey.shade200,
  23. child: Column(
  24. mainAxisSize: MainAxisSize.max,
  25. children: [
  26. _HeaderWidget(
  27. onFilterPressed: () {
  28. scaffoldKey.currentState?.openEndDrawer();
  29. },
  30. ),
  31. const SizedBox(height: 20),
  32. Expanded(child: _buildListView()),
  33. // Obx(
  34. // () {
  35. // if (controller.state.hasNextPage == false) {
  36. // return Container(
  37. // alignment: Alignment.center,
  38. // padding: const EdgeInsets.symmetric(vertical: 8),
  39. // child: const Text(
  40. // "没有更多数据了~",
  41. // style: TextStyle(color: Colors.grey, fontSize: 14),
  42. // ),
  43. // );
  44. // } else {
  45. // return const SizedBox();
  46. // }
  47. // },
  48. // ),
  49. ],
  50. ),
  51. ),
  52. );
  53. }
  54. Drawer _filterdrawer(BuildContext context) {
  55. return Drawer(
  56. shape: const RoundedRectangleBorder(
  57. borderRadius: BorderRadiusDirectional.horizontal(
  58. end: Radius.circular(0),
  59. ),
  60. ),
  61. width: MediaQuery.of(context).size.width * 0.3,
  62. child: Container(
  63. padding: const EdgeInsets.all(16),
  64. child: Column(
  65. crossAxisAlignment: CrossAxisAlignment.start,
  66. children: [
  67. const Text(
  68. '起始时间',
  69. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  70. ),
  71. const SizedBox(height: 8),
  72. Row(
  73. children: [
  74. Expanded(
  75. child: Obx(
  76. () => VDatePicker(
  77. value: controller.state.startTime.value,
  78. onChanged: (value) {
  79. controller.state.startTime.value = value;
  80. },
  81. ),
  82. ),
  83. ),
  84. const SizedBox(width: 8),
  85. Expanded(
  86. child: Obx(
  87. () => VDatePicker(
  88. value: controller.state.endTime.value,
  89. onChanged: (value) {
  90. controller.state.endTime.value = value;
  91. },
  92. ),
  93. ),
  94. ),
  95. ],
  96. ),
  97. const SizedBox(height: 16),
  98. ElevatedButton(
  99. onPressed: () {
  100. controller.reloadList(isFilter: true);
  101. Navigator.of(context).pop();
  102. },
  103. child: const Text('确定'),
  104. )
  105. ],
  106. ),
  107. ),
  108. );
  109. }
  110. Widget _buildListView() {
  111. final scrollController = ScrollController();
  112. scrollController.addListener(
  113. () {
  114. // 如果滑动到底部
  115. try {
  116. if (scrollController.position.atEdge) {
  117. if (scrollController.position.pixels != 0) {
  118. if (controller.state.hasNextPage) {
  119. controller.loadNextPageList();
  120. }
  121. }
  122. }
  123. } catch (e) {
  124. // logger.e("listViewScrollController exception:", e);
  125. }
  126. },
  127. );
  128. return RefreshIndicator(
  129. onRefresh: () async {
  130. controller.reloadList();
  131. },
  132. child: Obx(
  133. () {
  134. final list = controller.state.dataList;
  135. final children = <Widget>[];
  136. for (var i = 0; i < list.length; i++) {
  137. children.add(_PatientCard(dto: list[i]));
  138. }
  139. return Scrollbar(
  140. trackVisibility: true,
  141. controller: scrollController,
  142. child: GridView(
  143. shrinkWrap: true,
  144. controller: scrollController,
  145. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  146. crossAxisCount: 3,
  147. mainAxisSpacing: 16,
  148. crossAxisSpacing: 20,
  149. childAspectRatio: 360 / 180,
  150. ),
  151. children: children,
  152. ),
  153. );
  154. },
  155. ),
  156. );
  157. }
  158. }
  159. class _HeaderWidget extends GetView<PatientListController> {
  160. final VoidCallback onFilterPressed;
  161. const _HeaderWidget({
  162. required this.onFilterPressed,
  163. });
  164. @override
  165. Widget build(BuildContext context) {
  166. return SizedBox(
  167. height: 76,
  168. child: Row(
  169. children: [
  170. Expanded(
  171. child: SizedBox(
  172. height: 70,
  173. child: VSearchInput(
  174. placeholder: "身份证号码/姓名/手机号",
  175. onSearch: (value) {
  176. controller.state.searchString = value;
  177. controller.reloadList();
  178. },
  179. ),
  180. ),
  181. ),
  182. const SizedBox(width: 8),
  183. SizedBox(
  184. width: 180,
  185. height: 70,
  186. child: VButton(
  187. child: const Row(
  188. mainAxisAlignment: MainAxisAlignment.center,
  189. children: [
  190. Icon(Icons.note_add_outlined, size: 24),
  191. Text("新建档案", style: TextStyle(fontSize: 20)),
  192. ],
  193. ),
  194. onTap: () {
  195. controller.onCreateClicked();
  196. },
  197. ),
  198. ),
  199. const SizedBox(width: 8),
  200. SizedBox(
  201. width: 180,
  202. height: 70,
  203. child: VButton(
  204. onTap: onFilterPressed,
  205. child: const Row(
  206. mainAxisAlignment: MainAxisAlignment.center,
  207. children: [
  208. Icon(Icons.filter_alt, size: 24),
  209. Text("筛选", style: TextStyle(fontSize: 20)),
  210. ],
  211. ),
  212. ),
  213. ),
  214. ],
  215. ),
  216. );
  217. }
  218. }
  219. class _PatientCard extends StatelessWidget {
  220. final PatientDTO dto;
  221. const _PatientCard({required this.dto});
  222. @override
  223. Widget build(BuildContext context) {
  224. final body = Stack(
  225. children: [
  226. Container(
  227. padding: const EdgeInsets.symmetric(
  228. horizontal: 16,
  229. vertical: 12,
  230. ),
  231. child: Column(
  232. crossAxisAlignment: CrossAxisAlignment.start,
  233. children: [
  234. const SizedBox(height: 8),
  235. LayoutBuilder(
  236. builder: (context, c) {
  237. final width = c.maxWidth - 80 - 20;
  238. // 不和状态标签重叠,并保持一定距离
  239. return SizedBox(width: width, child: _buildBaseInfoRow());
  240. },
  241. ),
  242. const SizedBox(height: 12),
  243. _buildClassTags(),
  244. ],
  245. ),
  246. ),
  247. Positioned(
  248. top: 0,
  249. right: 0,
  250. child: _PatientSignStatusTag(
  251. dto: dto,
  252. ),
  253. ),
  254. ],
  255. );
  256. return Material(
  257. borderRadius: BorderRadius.circular(8),
  258. child: Ink(
  259. decoration: BoxDecoration(
  260. color: Colors.white,
  261. borderRadius: BorderRadius.circular(8),
  262. ),
  263. child: InkWell(
  264. borderRadius: BorderRadius.circular(8),
  265. onTap: () {
  266. Get.find<PatientListController>().gotoDetail(dto.code!);
  267. },
  268. child: body,
  269. ),
  270. ),
  271. );
  272. }
  273. Widget _buildBaseInfoRow() {
  274. final birthday = dto.birthday!.toLocal();
  275. final age = DataTimeUtils.calculateAge(birthday);
  276. return Row(
  277. mainAxisSize: MainAxisSize.max,
  278. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  279. children: [
  280. SizedBox(
  281. width: 100,
  282. child: Text(
  283. dto.patientName!,
  284. style: const TextStyle(
  285. color: Colors.black,
  286. fontSize: 20,
  287. fontWeight: FontWeight.bold,
  288. ),
  289. ),
  290. ),
  291. Expanded(
  292. child: Row(
  293. mainAxisSize: MainAxisSize.max,
  294. mainAxisAlignment: MainAxisAlignment.start,
  295. children: [
  296. Expanded(
  297. child: Text(
  298. RpcEnumLabels.gender[dto.patientGender] ?? "未知",
  299. style: const TextStyle(
  300. color: Colors.grey,
  301. fontSize: 20,
  302. fontWeight: FontWeight.bold,
  303. ),
  304. ),
  305. ),
  306. Expanded(
  307. child: Text(
  308. "$age岁",
  309. style: const TextStyle(
  310. color: Colors.grey,
  311. fontSize: 20,
  312. ),
  313. ),
  314. ),
  315. ],
  316. ),
  317. ),
  318. ],
  319. );
  320. }
  321. Widget _buildClassTags() {
  322. fn(String x) => Text(
  323. x,
  324. style: const TextStyle(color: Colors.grey, fontSize: 18),
  325. );
  326. return ConstrainedBox(
  327. constraints: const BoxConstraints(minWidth: double.infinity),
  328. child: Wrap(
  329. alignment: WrapAlignment.start,
  330. spacing: 20,
  331. runSpacing: 8,
  332. children: (dto.labelNames ?? []).map((e) => fn(e)).toList(),
  333. ),
  334. );
  335. }
  336. }
  337. class _PatientSignStatusTag extends StatelessWidget {
  338. final PatientDTO dto;
  339. const _PatientSignStatusTag({required this.dto});
  340. @override
  341. Widget build(BuildContext context) {
  342. return Container(
  343. alignment: Alignment.centerRight,
  344. width: 120,
  345. child: StatusLabel(
  346. title: contractStateTransition(dto.contractState),
  347. color: Theme.of(context).primaryColor,
  348. ),
  349. );
  350. }
  351. String contractStateTransition(ContractStateEnum state) {
  352. switch (state) {
  353. case ContractStateEnum.Unsigned:
  354. return "待签约";
  355. case ContractStateEnum.Cancelled:
  356. return "已解约";
  357. case ContractStateEnum.Expired:
  358. return "已过期";
  359. case ContractStateEnum.Signed:
  360. return "已签约";
  361. default:
  362. return "";
  363. }
  364. }
  365. }