123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- import React, { useEffect, useState, useRef } from 'react';
- import * as echarts from 'echarts';
- import styled from 'styled-components';
- import { apiGet } from '../../utils/request';
- import { useDefaultDate, getSelectedMonthRange } from '../../App';
- import useGlobalRefresh from '../../hooks/useGlobalRefresh';
- // 组件容器样式 - 与第一个模块保持一致
- const ChartWrapper = styled.div`
- width: 100%;
- height: 100%;
- position: relative;
- overflow: hidden;
- border: 1px solid #2980B9;
- `;
- // 组件标题 - 使用与其他模块相同的样式
- const PanelHeader = styled.div`
- position: relative;
- width: 95%;
- height: 1.6vw;
- padding-left: 5.5%;
- text-align: left;
- line-height: 1.6vw;
- font-size: 0.8vw;
- color: #E6F2FF;
- font-family: 'DingTalk JinBuTi', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
- background: url('/component_header_bg.png') no-repeat;
- background-size: 100% 100%;
- `;
- // 图表容器 - 统一与其他模块一致的布局
- const ChartContainer = styled.div`
- width: 100%;
- height: calc(100% - 1.6vw);
- padding: 0.5vw;
- box-sizing: border-box;
- overflow: hidden;
- `;
- // 数据接口类型 - 根据新接口字段定义
- interface WeeklyDrugData {
- weekDay: number; // 星期(数字)
- weekDayLabel: string; // 星期(文本)
- totalCount: number; // 日均发药盒数
- machineCount: number; // 日均机发盒数
- directSendRate: number; // 机发率
- }
- // 这个函数已不再需要,将在fetchData中直接使用getSelectedMonthRange
- const DailyMedicineQuantityChart: React.FC = () => {
- const chartRef = useRef<HTMLDivElement>(null);
- const chartInstance = useRef<echarts.ECharts | null>(null);
- const [data, setData] = useState<WeeklyDrugData[]>([]);
- const [loading, setLoading] = useState(true);
- const { defaultDateTime, selectedMonth } = useDefaultDate();
- // 获取数据
- const fetchData = async () => {
- try {
- setLoading(true);
- const { startTime, endTime } = getSelectedMonthRange(defaultDateTime, selectedMonth);
-
- const response = await apiGet('/stageStats/weeklyDrugOverview', {
- params: {
- startTime,
- endTime
- }
- });
-
- if (response.data && response.data.data && Array.isArray(response.data.data)) {
- setData(response.data.data);
- } else {
- console.warn('每周药品概况 - 接口返回格式不符合预期:', response.data);
- // 设置为空数组,不使用模拟数据
- setData([]);
- }
- } catch (error) {
- console.error('获取每周药品概况数据失败:', error);
- // 设置为空数组,不使用模拟数据
- setData([]);
- } finally {
- setLoading(false);
- }
- };
- // 初始化图表 - 双柱状图+折线图
- const initChart = () => {
- if (!chartRef.current || data.length === 0) return;
- // 销毁已存在的图表实例
- if (chartInstance.current) {
- chartInstance.current.dispose();
- }
- // 创建新的图表实例
- chartInstance.current = echarts.init(chartRef.current);
- const option = {
- legend: {
- data: ['日均发药盒数', '日均机发盒数', '机发率'],
- top: '2%',
- left: 'center',
- textStyle: {
- color: '#CCE6FF',
- fontSize: '0.6vw'
- },
- itemWidth: 10,
- itemHeight: 8,
- itemGap: 20
- },
- grid: {
- left: '0%',
- right: '0%',
- top: '15%',
- bottom: '5%',
- containLabel: true
- },
- xAxis: {
- type: 'category',
- data: data.map(item => item.weekDayLabel),
- axisLine: {
- lineStyle: {
- color: '#1F324D'
- }
- },
- axisLabel: {
- color: '#CCE6FF',
- fontSize: '0.6vw',
- margin: 5
- },
- axisTick: {
- show: false
- }
- },
- yAxis: [
- {
- type: 'value',
- position: 'left',
- max: function() {
- // 获取两个柱状图数据的最大值
- const maxValue = Math.max(
- ...data.map(item => Math.max(item.totalCount, item.machineCount))
- );
-
- // 如果最大值很小(小于100),设置max为最大值的1.5倍
- if (maxValue < 100) {
- return Math.ceil(maxValue * 1.5);
- }
-
- // 如果最大值较大,设置为1.2倍并向上取整到合适的数值
- const scaledMax = maxValue * 1.2;
-
- // 根据数值大小选择合适的取整方式
- if (scaledMax < 1000) {
- return Math.ceil(scaledMax / 100) * 100;
- } else if (scaledMax < 10000) {
- return Math.ceil(scaledMax / 500) * 500;
- } else {
- return Math.ceil(scaledMax / 1000) * 1000;
- }
- },
- axisLine: {
- show: false
- },
- axisTick: {
- show: false
- },
- axisLabel: {
- color: '#CCE6FF',
- fontSize: '0.55vw',
- },
- splitLine: {
- lineStyle: {
- color: '#1F324D',
- type: 'solid'
- }
- }
- },
- {
- type: 'value',
- position: 'right',
- min: function() {
- // 接口返回的是小数(0.65表示65%),动态计算显示范围
- const values = data.map(item => item.directSendRate);
- const minValue = Math.min(...values);
- const maxValue = Math.max(...values);
- const range = maxValue - minValue;
-
- // 如果数据范围很小,扩大显示范围
- if (range < 0.2) {
- return Math.max(0, minValue - 0.1);
- } else {
- return Math.max(0, minValue - range * 0.2);
- }
- },
- max: function() {
- const values = data.map(item => item.directSendRate);
- const minValue = Math.min(...values);
- const maxValue = Math.max(...values);
- const range = maxValue - minValue;
-
- // 如果数据范围很小,扩大显示范围
- if (range < 0.2) {
- return Math.min(1, maxValue + 0.1);
- } else {
- return Math.min(1, maxValue + range * 0.2);
- }
- },
- axisLine: {
- show: false
- },
- axisTick: {
- show: false
- },
- axisLabel: {
- color: '#CCE6FF',
- fontSize: '0.55vw',
- formatter: function(value: number) {
- return (value * 100).toFixed(0) + '%';
- }
- },
- splitLine: {
- show: false
- }
- }
- ],
- series: [
- {
- name: '日均发药盒数',
- type: 'bar',
- yAxisIndex: 0,
- data: data.map(item => item.totalCount),
- barWidth: '20%',
- barGap: '10%',
- itemStyle: {
- borderRadius: [3, 3, 0, 0],
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#66B3FF' },
- { offset: 1, color: 'rgba(102,179,255,0)' }
- ]),
- },
- emphasis: {
- itemStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#66B3FF' },
- { offset: 1, color: 'rgba(102,179,255,0.2)' }
- ])
- }
- },
- label: {
- show: false
- }
- },
- {
- name: '日均机发盒数',
- type: 'bar',
- yAxisIndex: 0,
- data: data.map(item => item.machineCount),
- barWidth: '20%',
- itemStyle: {
- borderRadius: [3, 3, 0, 0],
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#F3F9FF' },
- { offset: 1, color: 'rgba(243,249,255,0)' }
- ]),
- },
- emphasis: {
- itemStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#F3F9FF' },
- { offset: 1, color: 'rgba(243,249,255,0.2)' }
- ])
- }
- },
- label: {
- show: false
- }
- },
- {
- name: '机发率',
- type: 'line',
- yAxisIndex: 1,
- data: data.map(item => item.directSendRate),
- lineStyle: {
- color: '#99FFAA',
- width: 2
- },
- itemStyle: {
- color: '#99FFAA'
- },
- symbol: 'none',
- smooth: true
- }
- ],
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(15, 35, 85, 0.95)',
- borderColor: 'rgba(0, 212, 255, 0.6)',
- borderWidth: 1,
- textStyle: {
- color: '#FFFFFF',
- fontFamily: 'DingTalk JinBuTi',
- fontSize: '0.6vw'
- },
- formatter: (params: any) => {
- const totalData = params.find((p: any) => p.seriesName === '日均发药盒数');
- const machineData = params.find((p: any) => p.seriesName === '日均机发盒数');
- const lineData = params.find((p: any) => p.seriesType === 'line');
- const dataItem = data[totalData.dataIndex];
- return `<div style="padding: 0.3vw; font-size: 0.6vw; line-height: 1.4;">
- <div style="color: #00D4FF; font-weight: bold; margin-bottom: 0.3vw; font-size: 0.65vw;">${totalData.name}</div>
- <div style="margin-bottom: 0.2vw; display: flex; align-items: center;">
- <span style="color: #66B3FF; margin-right: 0.3vw; font-size: 0.8vw;">●</span>
- <span>日均发药盒数: </span>
- <span style="color: #FFFFFF; font-weight: bold; margin-left: 0.2vw;">${dataItem.totalCount}</span>
- </div>
- <div style="margin-bottom: 0.2vw; display: flex; align-items: center;">
- <span style="color: #F3F9FF; margin-right: 0.3vw; font-size: 0.8vw;">●</span>
- <span>日均机发盒数: </span>
- <span style="color: #FFFFFF; font-weight: bold; margin-left: 0.2vw;">${dataItem.machineCount}</span>
- </div>
- <div style="display: flex; align-items: center;">
- <span style="color: #99FFAA; margin-right: 0.3vw; font-size: 0.8vw;">●</span>
- <span>机发率: </span>
- <span style="color: #FFFFFF; font-weight: bold; margin-left: 0.2vw;">${(dataItem.directSendRate * 100).toFixed(1)}%</span>
- </div>
- </div>`;
- }
- }
- };
- chartInstance.current.setOption(option);
- };
- // 监听数据变化,重新渲染图表
- useEffect(() => {
- if (data.length > 0) {
- initChart();
- }
- }, [data]);
- // 组件挂载时获取数据
- useEffect(() => {
- fetchData();
- }, [defaultDateTime, selectedMonth]);
- // 监听窗口大小变化,确保图表自适应
- useEffect(() => {
- const handleResize = () => {
- if (chartInstance.current) {
- chartInstance.current.resize();
- }
- };
- window.addEventListener('resize', handleResize);
-
- // 使用ResizeObserver监听容器大小变化
- const resizeObserver = new ResizeObserver(() => {
- if (chartInstance.current) {
- chartInstance.current.resize();
- }
- });
-
- if (chartRef.current) {
- resizeObserver.observe(chartRef.current);
- }
- return () => {
- window.removeEventListener('resize', handleResize);
- resizeObserver.disconnect();
- if (chartInstance.current) {
- chartInstance.current.dispose();
- }
- };
- }, []);
- // 使用刷新Hook,配置对应的模块代码
- useGlobalRefresh(fetchData, '34');
- return (
- <ChartWrapper>
- <PanelHeader>每周药品概况</PanelHeader>
- <ChartContainer>
- <div
- ref={chartRef}
- style={{
- width: '100%',
- height: '100%',
- opacity: loading ? 0.5 : 1,
- transition: 'opacity 0.3s ease'
- }}
- />
- </ChartContainer>
- </ChartWrapper>
- );
- };
- export default DailyMedicineQuantityChart;
|