InventoryManagement.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import React, { useState, useEffect, useRef, useCallback } from 'react';
  2. import styled from 'styled-components';
  3. import ReactECharts from 'echarts-for-react';
  4. import * as echarts from 'echarts';
  5. import { apiGet } from '../../utils/request';
  6. import { getDefaultDate } from '../../App';
  7. import { PanelContainer } from '../styled/DashboardStyles';
  8. import useGlobalRefresh from '../../hooks/useGlobalRefresh';
  9. // 组件容器样式
  10. const Container = styled(PanelContainer)`
  11. width: 100%;
  12. height:42vh;
  13. padding: 0;
  14. position: relative;
  15. border-radius: 0.2vw;
  16. `;
  17. // 组件标题
  18. const PanelHeader = styled.div`
  19. position: relative;
  20. width: 65%;
  21. height: 1.6vw;
  22. padding-left: 4%;
  23. text-align: left;
  24. line-height: 1.6vw;
  25. font-size: 0.8vw;
  26. color: #CCE6FF;
  27. font-family: 'DingTalk JinBuTi', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  28. background: url('/component_header_bg.png') no-repeat;
  29. background-size: 100% 100%;
  30. `;
  31. // 内容容器
  32. const ContentBox = styled.div`
  33. flex: 1;
  34. display: flex;
  35. flex-direction: column;
  36. padding: 0.5vw;
  37. padding-top: 1.2vw;
  38. height: calc(100% - 1.6vw);
  39. `;
  40. // 设备标题
  41. const DeviceTitle = styled.div`
  42. font-size: 1.2vw;
  43. color: #CCE6FF;
  44. margin-bottom: 1vw;
  45. text-align: left;
  46. font-family: 'DingTalk JinBuTi', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  47. `;
  48. // 图表容器
  49. const ChartContainer = styled.div`
  50. flex: 1;
  51. display: flex;
  52. overflow: hidden;
  53. background: url('/kucunbeijing.png') no-repeat;
  54. background-size: 100% 100%;
  55. background-position: center;
  56. `;
  57. // 仪表盘容器
  58. const GaugeContainer = styled.div`
  59. width: 40%;
  60. height: 100%;
  61. display: flex;
  62. flex-direction: column;
  63. justify-content: center;
  64. align-items: center;
  65. position: relative;
  66. padding-right:4%;
  67. `;
  68. // 数据项列表
  69. const DataList = styled.div`
  70. width: 60%;
  71. display: flex;
  72. flex-wrap: wrap;
  73. `;
  74. // 数据项
  75. const DataItem = styled.div`
  76. width: 50%;
  77. height: 33.33%;
  78. display: flex;
  79. align-items: center;
  80. padding: 1vw 1vw;
  81. `;
  82. // 数据项图标
  83. const DataIcon = styled.div`
  84. width: 2vw;
  85. height: 2vw;
  86. border-radius: 0.3vw;
  87. display: flex;
  88. justify-content: center;
  89. align-items: center;
  90. margin-right: 1vw;
  91. `;
  92. // 数据项内容
  93. const DataContent = styled.div`
  94. display: flex;
  95. flex-direction: column;
  96. justify-content: center;
  97. align-items: flex-start;
  98. `;
  99. // 数据项标题
  100. const DataTitle = styled.div`
  101. font-size: 0.8vw;
  102. color: #CCE6FF;
  103. margin-bottom: 0.2vw;
  104. `;
  105. // 数据项值
  106. const DataValue = styled.div`
  107. font-size: 2.3vw;
  108. color: #FBFDFF;
  109. font-family: 'DingTalk JinBuTi', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  110. `;
  111. interface InventoryData {
  112. totalStock: number;
  113. equipTotalStock: number;
  114. equipInputStock: number | null;
  115. equipOutputStock: number | null;
  116. equipDrugVarietyNum: number;
  117. expectedAvailableDays: number;
  118. equipOutStockDrugVariety: number;
  119. }
  120. // 新增类型定义
  121. type EquipTotalStockItem = {
  122. name: string;
  123. value: number;
  124. color?: string;
  125. };
  126. interface InventoryManagementProps {
  127. selectedEquipCode: string;
  128. selectedEquipName: string;
  129. }
  130. const InventoryManagement: React.FC<InventoryManagementProps> = ({ selectedEquipCode, selectedEquipName }) => {
  131. const chartRef = useRef<ReactECharts>(null);
  132. const [data, setData] = useState<InventoryData>({
  133. totalStock: 0,
  134. equipTotalStock: 0,
  135. equipInputStock: null,
  136. equipOutputStock: null,
  137. equipDrugVarietyNum: 0,
  138. expectedAvailableDays: 0,
  139. equipOutStockDrugVariety: 0
  140. });
  141. const [chartSize, setChartSize] = useState({ width: 300, height: 300 });
  142. const [equipTotalStockList, setEquipTotalStockList] = useState<EquipTotalStockItem[]>([]);
  143. // 监听容器大小变化
  144. useEffect(() => {
  145. const updateSize = () => {
  146. if (chartRef.current) {
  147. const container = chartRef.current.ele;
  148. if (container) {
  149. const newWidth = container.clientWidth;
  150. const newHeight = container.clientHeight;
  151. // 只有当尺寸真正变化且不为0时才更新状态
  152. if (newWidth > 0 && newHeight > 0 &&
  153. (Math.abs(newWidth - chartSize.width) > 5 ||
  154. Math.abs(newHeight - chartSize.height) > 5)) {
  155. setChartSize({
  156. width: newWidth,
  157. height: newHeight
  158. });
  159. }
  160. }
  161. }
  162. };
  163. // 初始更新
  164. setTimeout(updateSize, 300); // 给图表一些渲染时间
  165. // 监听窗口大小变化
  166. window.addEventListener('resize', updateSize);
  167. return () => {
  168. window.removeEventListener('resize', updateSize);
  169. };
  170. }, [chartSize.width, chartSize.height]);
  171. const fetchData = useCallback(async () => {
  172. try {
  173. // 获取默认日期
  174. const defaultDate = getDefaultDate();
  175. // 直接使用选中的设备代码
  176. const equipCode = selectedEquipCode;
  177. // 调用新的库存管理接口
  178. const response = await apiGet('/equipMana/equipInventoryInfo', {
  179. params: {
  180. dateTime: defaultDate,
  181. equipCode: equipCode
  182. }
  183. });
  184. if (response.data && response.data.data) {
  185. setData({
  186. totalStock: response.data.data.totalStock || 0,
  187. equipTotalStock: response.data.data.equipTotalStock || 0,
  188. equipInputStock: response.data.data.equipInputStock,
  189. equipOutputStock: response.data.data.equipOutputStock,
  190. equipDrugVarietyNum: response.data.data.equipDrugVarietyNum || 0,
  191. expectedAvailableDays: response.data.data.expectedAvailableDays || 0,
  192. equipOutStockDrugVariety: response.data.data.equipOutStockDrugVariety || 0
  193. });
  194. // 修正:适配equipTotalStockList字段
  195. if (Array.isArray(response.data.data.equipTotalStockList)) {
  196. const colorArr = ['#4D88FF', '#FFE980', '#4DE1FF', '#8FADCC', '#FFB980', '#B6A2DE', '#5AB1EF', '#FF6666'];
  197. setEquipTotalStockList(
  198. response.data.data.equipTotalStockList.map((item: any, idx: number) => ({
  199. name: item.equipCode ? `设备${item.equipCode}` : `设备${idx+1}`,
  200. value: item.quantity,
  201. color: colorArr[idx % colorArr.length]
  202. }))
  203. );
  204. } else {
  205. setEquipTotalStockList([]);
  206. }
  207. }
  208. } catch (error) {
  209. console.error('获取库存数据失败:', error);
  210. // 使用模拟数据(仅用于开发)
  211. setData({
  212. totalStock: 19352,
  213. equipTotalStock: 16856,
  214. equipInputStock: null,
  215. equipOutputStock: null,
  216. equipDrugVarietyNum: 686,
  217. expectedAvailableDays: 0.0,
  218. equipOutStockDrugVariety: 0
  219. });
  220. setEquipTotalStockList([
  221. { name: '蓝色部分', value: 25, color: '#4D88FF' },
  222. { name: '黄色部分', value: 25, color: '#FFE980' },
  223. { name: '青色部分', value: 25, color: '#4DE1FF' },
  224. { name: '淡蓝色部分', value: 25, color: '#8FADCC' }
  225. ]);
  226. }
  227. }, [selectedEquipCode]);
  228. // 组件挂载时获取数据
  229. useEffect(() => {
  230. fetchData();
  231. }, [fetchData]);
  232. // 当选中的设备代码变化时重新获取数据
  233. useEffect(() => {
  234. fetchData();
  235. }, [selectedEquipCode, fetchData]);
  236. // 使用全局刷新钩子,指定模块代码'25'
  237. useGlobalRefresh(fetchData, '25');
  238. // 获取饼图配置
  239. const getPieOption = () => {
  240. // 计算自适应的边框宽度,基于容器大小,但不小于1
  241. const adaptiveBorderWidth = Math.max(1, Math.min(4, chartSize.width * 0.01));
  242. // 计算字体大小,但设置最小值避免过小
  243. const titleFontSize = Math.max(12, chartSize.width * 0.05);
  244. const valueFontSize = Math.max(20, chartSize.width * 0.1);
  245. const paddingBottom = Math.max(5, chartSize.width * 0.015);
  246. // 饼图缩放比例 - 可以调整这个值来改变饼图大小
  247. const pieScale = 0.8; // 1.0为标准大小,可增大或减小
  248. // 环宽度控制参数
  249. const ringWidth = 0.3; // 环宽度,值越大环越宽,值越小环越窄
  250. const outerRadius = 90 * pieScale; // 外圆半径
  251. const innerRadius = outerRadius - (outerRadius * ringWidth); // 内圆半径
  252. // 内圆大小控制参数
  253. const innerCircleScale = 0.45 * pieScale; // 内部黑色圆的大小
  254. // 饼图数据动态生成
  255. const pieData = (equipTotalStockList && equipTotalStockList.length > 0)
  256. ? (equipTotalStockList as EquipTotalStockItem[]).map(function(item: EquipTotalStockItem, idx: number) {
  257. return {
  258. value: item.value,
  259. name: item.name,
  260. itemStyle: { color: item.color || ['#4D88FF', '#FFE980', '#4DE1FF', '#8FADCC'][idx % 4] }
  261. };
  262. })
  263. : [];
  264. // 如果没有数据,只显示中心label,不渲染pie片
  265. if (!pieData.length) {
  266. return {
  267. backgroundColor: 'transparent',
  268. series: [
  269. {
  270. type: 'pie',
  271. radius: ['0%', '100%'],
  272. center: ['50%', '50%'],
  273. label: {
  274. position: 'center',
  275. formatter: function() {
  276. return '{title|总库存}\n{value|' + data.totalStock + '}';
  277. },
  278. rich: {
  279. title: {
  280. color: '#CCE6FF',
  281. fontSize: titleFontSize,
  282. padding: [0, 0, paddingBottom, 0]
  283. },
  284. value: {
  285. color: '#fff',
  286. fontSize: valueFontSize,
  287. fontWeight: 'bold',
  288. fontFamily: 'DingTalk JinBuTi, PingFang SC, Microsoft YaHei, Arial, sans-serif'
  289. }
  290. }
  291. },
  292. data: [{ value: 100, name: '背景', itemStyle: { color: '#041529' } }],
  293. itemStyle: { color: '#041529' },
  294. labelLine: { show: false },
  295. emphasis: { scale: false }
  296. }
  297. ]
  298. };
  299. }
  300. return {
  301. backgroundColor: 'transparent',
  302. tooltip: {
  303. trigger: 'item',
  304. formatter: '{b}: {c} ({d}%)',
  305. textStyle: { fontSize: Math.max(12, chartSize.width * 0.04) }
  306. },
  307. series: [
  308. {
  309. type: 'pie',
  310. radius: [`${innerRadius}%`, `${outerRadius}%`],
  311. center: ['50%', '50%'],
  312. startAngle: 0,
  313. avoidLabelOverlap: false,
  314. label: { show: false },
  315. emphasis: { scale: false },
  316. itemStyle: {
  317. borderWidth: adaptiveBorderWidth,
  318. borderColor: '#041529'
  319. },
  320. data: pieData
  321. },
  322. {
  323. type: 'pie',
  324. radius: ['0', `${innerCircleScale * 100}%`],
  325. center: ['50%', '50%'],
  326. label: {
  327. position: 'center',
  328. formatter: function() {
  329. return '{title|总库存}\n{value|' + data.totalStock + '}';
  330. },
  331. rich: {
  332. title: {
  333. color: '#CCE6FF',
  334. fontSize: titleFontSize,
  335. padding: [0, 0, paddingBottom, 0]
  336. },
  337. value: {
  338. color: '#fff',
  339. fontSize: valueFontSize,
  340. fontWeight: 'bold',
  341. fontFamily: 'DingTalk JinBuTi, PingFang SC, Microsoft YaHei, Arial, sans-serif'
  342. }
  343. }
  344. },
  345. itemStyle: {
  346. color: '#041529'
  347. },
  348. data: [
  349. { value: 100, name: '背景' }
  350. ]
  351. }
  352. ]
  353. };
  354. };
  355. return (
  356. <Container>
  357. <PanelHeader>库存管理</PanelHeader>
  358. <ContentBox>
  359. <DeviceTitle>设备: {selectedEquipName}</DeviceTitle>
  360. <ChartContainer>
  361. <GaugeContainer>
  362. <ReactECharts
  363. ref={chartRef}
  364. option={getPieOption()}
  365. style={{ height: '100%', width: '100%' }}
  366. opts={{ renderer: 'canvas' }}
  367. notMerge={true}
  368. />
  369. </GaugeContainer>
  370. <DataList>
  371. <DataItem>
  372. <DataIcon>
  373. <img src="/kucunicon.png" alt="库存图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  374. </DataIcon>
  375. <DataContent>
  376. <DataTitle>库存</DataTitle>
  377. <DataValue>{data.equipTotalStock}</DataValue>
  378. </DataContent>
  379. </DataItem>
  380. <DataItem>
  381. <DataIcon>
  382. <img src="/zaikupinzgingshuicon.png" alt="在库品种数图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  383. </DataIcon>
  384. <DataContent>
  385. <DataTitle>设备在库品种数</DataTitle>
  386. <DataValue>{data.equipDrugVarietyNum}</DataValue>
  387. </DataContent>
  388. </DataItem>
  389. <DataItem>
  390. <DataIcon>
  391. <img src="/shebeirukuheshuicon.png" alt="设备入库盒数图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  392. </DataIcon>
  393. <DataContent>
  394. <DataTitle>设备入库盒数</DataTitle>
  395. <DataValue>{data.equipInputStock || 0}</DataValue>
  396. </DataContent>
  397. </DataItem>
  398. <DataItem>
  399. <DataIcon>
  400. <img src="/shebeichukuheshuicon.png" alt="设备出库盒数图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  401. </DataIcon>
  402. <DataContent>
  403. <DataTitle>设备出库盒数</DataTitle>
  404. <DataValue>{data.equipOutputStock || 0}</DataValue>
  405. </DataContent>
  406. </DataItem>
  407. <DataItem>
  408. <DataIcon>
  409. <img src="/yugexiaohaotianshuicon.png" alt="预估可消耗天数图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  410. </DataIcon>
  411. <DataContent>
  412. <DataTitle>预估可消耗天数</DataTitle>
  413. <DataValue>{data.expectedAvailableDays}</DataValue>
  414. </DataContent>
  415. </DataItem>
  416. <DataItem>
  417. <DataIcon>
  418. <img src="/queyaolvicon.png" alt="缺药率图标" style={{ width: '2.5vw', height: '2.5vw' }} />
  419. </DataIcon>
  420. <DataContent>
  421. <DataTitle>缺药率&gt;90%品种数</DataTitle>
  422. <DataValue>{data.equipOutStockDrugVariety}</DataValue>
  423. </DataContent>
  424. </DataItem>
  425. </DataList>
  426. </ChartContainer>
  427. </ContentBox>
  428. </Container>
  429. );
  430. };
  431. export default InventoryManagement;