BasicLayout.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // BasicLayout.tsx
  2. import React from 'react';
  3. import { useSelector, useDispatch } from 'react-redux';
  4. import { Layout, Row, Col, ConfigProvider } from 'antd';
  5. import { TabBar } from 'antd-mobile';
  6. import logo from '@/assets/imgs/logo-v3.jpg';
  7. import NavbarFloat from './NavbarFloat';
  8. import NavMenu from './NavMenu';
  9. import BottomBar from './BottomBar';
  10. import StatusBar, { StatusBarProps } from './StateBar';
  11. import RegisterPage from '@/pages/patient/register';
  12. import WorklistPage from '@/pages/patient/worklist';
  13. import HistorylistPage from '@/pages/patient/HistoryList';
  14. import ArchivelistPage from '@/pages/patient/ArchiveList';
  15. import BinPage from '@/pages/patient/Bin';
  16. import OutputlistPage from '@/pages/patient/OutputList';
  17. import PatientManagement from '@/pages/patient/PatientManagement';
  18. import MeButton from '@/pages/security/components/MeButton';
  19. import Profile from '@/pages/security/Profile';
  20. import ExamPage from '@/pages/exam/ExamPage';
  21. import { RootState } from '@/states/store';
  22. import store from '@/states/store';
  23. import { setBusinessFlow } from '@/states/BusinessFlowSlice';
  24. import ImageProcessingPage from '@/pages/view/ImageProcessingPage';
  25. import PrintPage from '@/pages/output/print/PrintPage';
  26. import { pageLayoutConfig } from '@/config/pageLayout';
  27. import selectedWorksToPrint from '@/domain/patient/selectedWorksToPrint';
  28. import { message } from 'antd';
  29. // import { Link } from 'react-router-dom';
  30. // import { MenuOutlined } from '@ant-design/icons';
  31. // const { Content, Footer } = Layout;
  32. interface BasicLayoutProps {
  33. children: React.ReactNode;
  34. }
  35. // //自定义breakpoint
  36. // const breakpoint = {
  37. // xs: 480, // 小屏幕设备(手机)
  38. // sm: 576, // 中小屏幕设备(平板)
  39. // md: 768, // 中等屏幕设备(小桌面显示器)
  40. // lg: 992, // 大屏幕设备(桌面显示器)
  41. // xl: 1200, // 超大屏幕设备(大桌面显示器)
  42. // xxl: 1600 // 超超大屏幕设备(超大桌面显示器)
  43. // };
  44. const BasicLayout: React.FC<BasicLayoutProps> = () => {
  45. const status: StatusBarProps = {
  46. diskStatus: 'available', // 可用状态
  47. batteryLevel: 0,
  48. wifiStrength: 0,
  49. heatCapacity: 0,
  50. };
  51. const dispatch = useDispatch();
  52. const currentKey = useSelector(
  53. (state: RootState) => state.BusinessFlow.currentKey
  54. );
  55. // key和内容组件的映射
  56. const contentMap = {
  57. exam: <ExamPage />,
  58. process: <ImageProcessingPage />,
  59. view: <ImageProcessingPage />,
  60. register: <RegisterPage />,
  61. worklist: <WorklistPage />,
  62. historylist: <HistorylistPage />,
  63. archivelist: <ArchivelistPage />,
  64. bin: <BinPage />,
  65. outputlist: <OutputlistPage />,
  66. patient_management: <PatientManagement />,
  67. profile: <Profile />, // 需确保已引入 Profile 组件
  68. print: <PrintPage />,
  69. };
  70. //感知菜单项点击
  71. const handleMenuClick = async (key: string) => {
  72. // 特殊处理打印按钮
  73. // todo 这一段逻辑后续可以考虑放到 middleware 里处理,即 businessFlowMiddlewareLogic
  74. if (key === 'print') {
  75. if (currentKey === 'worklist' || currentKey === 'historylist') {
  76. // 获取当前选中的记录
  77. const state = store.getState();
  78. const selectedIds =
  79. currentKey === 'historylist'
  80. ? state.historySelection.selectedIds
  81. : currentKey === 'worklist'
  82. ? state.workSelection.selectedIds
  83. : [];
  84. if (selectedIds.length === 0) {
  85. message.warning('请先选择要打印的项目');
  86. return;
  87. }
  88. try {
  89. // 调用 selectedWorksToPrint 加载数据并跳转
  90. await selectedWorksToPrint(selectedIds, currentKey as 'worklist' | 'historylist');
  91. } catch (error) {
  92. console.error('[BusinessZone] Print error:', error);
  93. message.error('加载打印数据失败,请重试');
  94. }
  95. } else if(currentKey === 'exam' || currentKey === 'process') {
  96. // 其他按钮直接切换页面
  97. dispatch(setBusinessFlow(key));
  98. }
  99. }
  100. else {
  101. // 其他按钮直接切换页面
  102. dispatch(setBusinessFlow(key));
  103. }
  104. };
  105. return (
  106. // ConfigProvider 用于自定义 Ant Design 的主题和 breakpoints
  107. <ConfigProvider
  108. theme={
  109. {
  110. // token: {
  111. // // 自定义屏幕尺寸断点,与要求匹配
  112. // screenXS: 576, // xs: <576px
  113. // screenSM: 768, // sm: ≥768px
  114. // screenMD: 992, // md: ≥992px
  115. // screenLG: 1200, // lg: ≥1200px
  116. // screenXL: 1400, // xl: ≥1400px
  117. // screenXXL: 1600, // xxl: ≥1600px
  118. // },
  119. }
  120. }
  121. >
  122. <Layout
  123. style={{
  124. minHeight: '100vh',
  125. // border: '1px solid #ccc'
  126. }}
  127. className="h-[100vh] xl:text-[48px] lg:text-[40px] md:text-[30px] sm:text-[20px] xs:text-[15px]"
  128. >
  129. {/* 第一行:品牌和状态区域 */}
  130. {pageLayoutConfig.brandAndStatusPosition === 'top' && (
  131. <Row
  132. className="xl:h-[9vh] text-[100%]"
  133. style={{ borderBottom: '1px solid #e5e7eb' }}
  134. >
  135. {/* 品牌区域 */}
  136. <Col xl={4} lg={4} md={5} sm={0} xs={0} className="h-full">
  137. <img
  138. data-testid="logo-image"
  139. src={logo}
  140. alt="Logo"
  141. className="h-full w-auto object-contain"
  142. />
  143. </Col>
  144. {/* 状态区域 */}
  145. <Col
  146. xl={20}
  147. lg={20}
  148. md={19}
  149. sm={0}
  150. xs={0}
  151. className="items-center
  152. justify-end xl:flex
  153. lg:flex
  154. md:flex
  155. sm:hidden
  156. xs:hidden
  157. h-full
  158. text-[100%]"
  159. >
  160. {/* text-[100%] 用于传导父元素的字体大小,最终影响图标大小 */}
  161. <StatusBar
  162. diskStatus={status.diskStatus}
  163. batteryLevel={status.batteryLevel}
  164. wifiStrength={status.wifiStrength}
  165. heatCapacity={status.heatCapacity}
  166. />
  167. </Col>
  168. </Row>
  169. )}
  170. {/* 第二行:导航区域(第1列)和内容区域(第2列) */}
  171. <Row
  172. style={{
  173. flex: 1,
  174. display: 'flex',
  175. // paddingTop: '24px', // 顶部留白
  176. // border: '1px solid red',
  177. }}
  178. className="xxl:h-[90vh] xl:h-[90vh] lg:h-[90vh] md:h-[90vh] sm:h-[90vh] xs:h-[90vh] pt-2"
  179. >
  180. {/* 导航区域 */}
  181. <Col xl={2} lg={0} md={0} sm={0} xs={0} className="h-full">
  182. <NavMenu onMenuClick={handleMenuClick} />
  183. </Col>
  184. {/* 内容区域 */}
  185. <Col
  186. xl={22}
  187. lg={24}
  188. xs={24}
  189. md={24}
  190. sm={24}
  191. style={{
  192. flex: 1,
  193. // border: '1px solid blue'
  194. }}
  195. className="h-full"
  196. >
  197. {/* {children} */}
  198. {contentMap[currentKey]}
  199. </Col>
  200. </Row>
  201. {/* 第一行:品牌和状态区域 */}
  202. {pageLayoutConfig.brandAndStatusPosition === 'bottom' && (
  203. <Row
  204. className="xl:h-[9vh] md:h-[9vh] text-[100%]"
  205. style={{ borderBottom: '1px solid #e5e7eb' }}
  206. >
  207. {/* 品牌区域 */}
  208. <Col xl={8} lg={8} md={5} sm={0} xs={0} className="h-full">
  209. {/* 底部通栏:仅在桌面端显示 */}
  210. <BottomBar />
  211. </Col>
  212. {/* 状态区域 */}
  213. <Col
  214. xl={16}
  215. lg={16}
  216. md={19}
  217. sm={0}
  218. xs={0}
  219. className="items-center
  220. justify-end xl:flex
  221. lg:flex
  222. md:flex
  223. sm:hidden
  224. xs:hidden
  225. h-full
  226. text-[100%]"
  227. >
  228. {/* text-[100%] 用于传导父元素的字体大小,最终影响图标大小 */}
  229. <StatusBar
  230. diskStatus={status.diskStatus}
  231. batteryLevel={status.batteryLevel}
  232. wifiStrength={status.wifiStrength}
  233. heatCapacity={status.heatCapacity}
  234. />
  235. </Col>
  236. </Row>
  237. )}
  238. {/* Tabbar:固定在底部,仅在手机屏幕显示 */}
  239. <div className="sticky bottom-0 w-full bg-red xl:hidden">
  240. <TabBar
  241. className="xl:hidden lg:hidden md:hidden sm:block xs:block"
  242. onChange={(key) => handleMenuClick(key)}
  243. >
  244. <TabBar.Item
  245. className="text-red-500"
  246. title="患者管理"
  247. key="patient_management"
  248. icon={<div>🏠</div>}
  249. />
  250. <TabBar.Item
  251. className="text-red-500"
  252. title="检查"
  253. key="examination"
  254. icon={<div>👤</div>}
  255. />
  256. <TabBar.Item
  257. className="text-red-500"
  258. title="急诊"
  259. key="emergency"
  260. icon={<div>⚙️</div>}
  261. />
  262. <TabBar.Item
  263. className="text-red-500"
  264. title="图像处理"
  265. key="process"
  266. icon={<div>⚙️</div>}
  267. />
  268. <TabBar.Item
  269. className="text-red-500"
  270. title="打印"
  271. key="print"
  272. icon={<div>⚙️</div>}
  273. />
  274. <TabBar.Item
  275. title="我的"
  276. key="profile"
  277. icon={
  278. <MeButton
  279. isLogin={true} // TODO: 替换为实际登录状态
  280. avatarUrl={undefined} // TODO: 替换为实际头像
  281. onClick={() => handleMenuClick('profile')}
  282. // 不传递username
  283. />
  284. }
  285. />
  286. </TabBar>
  287. </div>
  288. <NavbarFloat
  289. className="fixed hidden sm:hidden xs:hidden md:block lg:block xl:hidden"
  290. position="left"
  291. onMenuClick={handleMenuClick}
  292. />
  293. </Layout>
  294. </ConfigProvider>
  295. );
  296. };
  297. export default BasicLayout;