TVscreen/QYJY/index.html

497 lines
19 KiB
HTML
Raw Permalink Normal View History

2026-09-18 15:13:13 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
const cssVer = Date.now();
// 直接document.write输出带版本的link,写在head内,不会闪屏
document.write(`
<link rel="stylesheet" href="./css/icon.css">
<link rel="stylesheet" href="./css/index.css?v=${cssVer}">
<link rel="stylesheet" href="./css/public.css">
`);
</script>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no" />
<title>区域检验数据中心</title>
<!-- css/js增加版本号防缓存 -->
<!-- <link rel="stylesheet" href="./css/icon.css" />
<link rel="stylesheet" href="./css/index.css" />
<link rel="stylesheet" href="./css/public.css" /> -->
</head>
<body>
<div class="bg">
<div class="title">区域检验数据中心</div>
<div class="container">
<div class="leftMain">
<div class="leftMain_top">
<div class="leftMain_topIn">
<div class="title_nums">当月累计数据</div>
<ul id="sourceMonthWrap"></ul>
<div class="title_nums">当年累计数据</div>
<ul id="sourceYearWrap"></ul>
</div>
</div>
<div class="leftMain_middle">
<div class="leftMain_middle_left">
<div class="leftMain_middle_leftIn">
<h3>委托机构当月送检标本数量</h3>
<div class="biaoge">
<div id="chartmain" style="width:100%;height:100%;"></div>
</div>
<span class="border_bg_leftTop"></span>
<span class="border_bg_rightTop"></span>
<span class="border_bg_leftBottom"></span>
<span class="border_bg_rightBottom"></span>
</div>
<div class="leftMain_middle_leftIn leftMain_middle_leftIn_zhe">
<h3>当年送检标本数量趋势图</h3>
<div class="biaoge">
<div id="chartmain_zhe" style="width:100%;height:100%;"></div>
</div>
<span class="border_bg_leftTop"></span>
<span class="border_bg_rightTop"></span>
<span class="border_bg_leftBottom"></span>
<span class="border_bg_rightBottom"></span>
</div>
</div>
<div class="leftMain_middle_right">
<div class="leftMain_middle_rightIn">
<h3>当月标本全流程监控情况</h3>
<div class="biaoge biaoge_pai">
<div class="biaoge_paiIn">
<ul id="dayCountsWrap"></ul>
</div>
</div>
<span class="border_bg_leftTop"></span>
<span class="border_bg_rightTop"></span>
<span class="border_bg_leftBottom"></span>
<span class="border_bg_rightBottom"></span>
</div>
</div>
</div>
</div>
<div class="rightMain">
<div class="rightMain_topIn">
<h3>委托机构当月送检标本占比</h3>
<div class="proportion">
<ul id="listDataWrap"></ul>
</div>
<span class="border_bg_leftTop"></span>
<span class="border_bg_rightTop"></span>
<span class="border_bg_leftBottom"></span>
<span class="border_bg_rightBottom"></span>
</div>
</div>
</div>
</div>
<script src="./js/echarts.min.js"></script>
<script>
// 全局变量
let echarts1Instance = null;
let echarts3Instance = null;
// 基础数据模板
const sourceMonth = [
{ name: '区域机构', num: 0, unit: '家', pre: 'jgs' },
{ name: '检验人数', num: 0, unit: '人', pre: 'rs' },
{ name: '检验管数', num: 0, unit: '个', pre: 'gs' },
{ name: '检验报告数', num: 0, unit: '份', pre: 'bgs' },
];
const sourceYear = [
{ name: '区域机构', num: 0, unit: '家', pre: 'jgs' },
{ name: '检验人数', num: 0, unit: '人', pre: 'rs' },
{ name: '检验管数', num: 0, unit: '个', pre: 'gs' },
{ name: '检验报告数', num: 0, unit: '份', pre: 'bgs' },
];
let dayCounts = [
{ dict_name: '检验申请', sl: 0, percent: '0%', flag: false },
{ dict_name: '标本采集', sl: 0, percent: '0%', remain: 0, remainPer: '0%', flag: true },
{ dict_name: '标本送检', sl: 0, percent: '0%', remain: 0, remainPer: '0%', flag: true },
{ dict_name: '标本签收', sl: 0, percent: '0%', remain: 0, remainPer: '0%', flag: true },
{ dict_name: '上机检测', sl: 0, percent: '0%', remain: 0, remainPer: '0%', flag: true },
{ dict_name: '结果审核', sl: 0, percent: '0%', remain: 0, remainPer: '0%', flag: true },
{ dict_name: '申请作废', sl: 0, percent: '0%', flag: false },
{ dict_name: '标本退回', sl: 0, percent: '0%', flag: false },
];
let listData = [];
// 折线图轮播变量
let lineTipTimer = null;
let lineCurrentIndex = -1;
const LINE_TIP_INTERVAL = 2500;
function startLineTipLoop() {
if (lineTipTimer) clearInterval(lineTipTimer);
lineCurrentIndex = -1;
lineTipTimer = setInterval(() => {
if (!echarts3Instance) return;
const opt = echarts3Instance.getOption();
const dataLen = opt.xAxis[0].data.length;
if (dataLen === 0) return;
lineCurrentIndex = (lineCurrentIndex + 1) % dataLen;
opt.series.forEach((_, idx) => echarts3Instance.dispatchAction({ type: 'downplay', seriesIndex: idx }));
opt.series.forEach((_, idx) => echarts3Instance.dispatchAction({ type: 'highlight', seriesIndex: idx, dataIndex: lineCurrentIndex }));
echarts3Instance.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: lineCurrentIndex });
}, LINE_TIP_INTERVAL);
}
function stopLineTipLoop() {
if (lineTipTimer) clearInterval(lineTipTimer);
lineTipTimer = null;
}
// 渲染列表函数
function renderNumList(containerId, arr) {
const wrap = document.getElementById(containerId);
wrap.innerHTML = '';
arr.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `
<div class="liIn">
<div class="nums_box">
<h3>${item.name}</h3>
<p class="shu"><span class="shu1">${item.num}</span><i>${item.unit}</i></p>
</div>
<span class="border_bg_leftTop"></span>
<span class="border_bg_rightTop"></span>
<span class="border_bg_leftBottom"></span>
<span class="border_bg_rightBottom"></span>
</div>
`;
wrap.appendChild(li);
})
}
function renderDayCounts() {
const wrap = document.getElementById('dayCountsWrap');
wrap.innerHTML = '';
dayCounts.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `
<div class="liIn">
<div class="liIn_left"><span class="zi">${item.dict_name}</span></div>
<div class="liIn_line">
<div class="line_lineIn" style="width:${item.percent}">
<div class="per">${item.percent}</div>
</div>
</div>
<p class="num">${item.sl}</p>
<div class="clear"></div>
</div>
<div class="liIn" style="display:${item.flag ? 'flex' : 'none'}">
<div class="liIn_left"><span class="zi">&nbsp;</span></div>
<div class="liIn_line">
<div class="line_lineIn_sy" style="width:${item.remainPer}">
<div class="per">剩余${item.remain}</div>
</div>
</div>
<p class="num">&nbsp;</p>
<div class="clear"></div>
</div>
`;
wrap.appendChild(li);
})
}
function renderListData() {
const wrap = document.getElementById('listDataWrap');
wrap.innerHTML = '';
listData.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `
<div class="liIn">
<div class="liIn_left"><span class="zi" title="${item.groupname}">${item.groupname}</span></div>
<div class="liIn_line">
<div class="line_lineIn" style="width:${item.percentage}">
<div class="per">${item.percentage}</div>
</div>
</div>
<p class="num">${item.groupsum}</p>
<div class="clear"></div>
</div>
`;
wrap.appendChild(li);
})
}
// ========= 只初始化一次柱状图,不再每次刷新销毁重建 =========
function initBarChart() {
const dom = document.getElementById('chartmain');
echarts1Instance = echarts.init(dom, null, { devicePixelRatio: window.devicePixelRatio });
const option = {
title: { text: '' },
grid: { x: 30, y: 30, x2: 10, y2: 20 },
tooltip: {
trigger: 'axis',
show: false,
backgroundColor: 'rgba(0,18,36,0.9)',
textStyle: { color: '#fff', fontSize: 14 },
padding: [12, 16],
borderWidth: 1,
borderColor: '#0efdff'
},
label: {
show: true,
color: '#fff',
fontSize: 16,
fontWeight: 700,
position: 'top',
//没有数据 0的情况不显示
formatter: function (params) {
if (params.value == 0) {
return '';
}
return params.value;
}
},
xAxis: {
data: [],
axisLabel: { interval: 0, color: '#fff', fontSize: 12 },
axisTick: { show: false },
axisLine: { show: true, symbol: ['none', 'arrow'], symbolOffset: 12, lineStyle: { color: '#fff' } },
z: 10
},
yAxis: {
type: 'value', name: '单位:个', minInterval: 1,
axisLine: { show: true, symbol: ['none', 'arrow'], symbolOffset: 12, lineStyle: { color: '#fff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 12 }
},
series: [{
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1,
[{ offset: 0, color: '#0efdff' }, { offset: 0.5, color: '#188df0' }, { offset: 1, color: '#188df0' }]
)
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1,
[{ offset: 0, color: '#2378f7' }, { offset: 0.7, color: '#2378f7' }, { offset: 1, color: '#0efdff' }]
)
}
},
data: []
}]
};
echarts1Instance.setOption(option);
}
// 仅更新柱状图数据,不销毁实例
function updateBarData(data) {
const dataAxis = data.map(item => item.daynum);
const dataVal = data.map(item => item.sl);
echarts1Instance.setOption({ xAxis: { data: dataAxis }, series: [{ data: dataVal }] });
}
// 基准:1920宽度下字号12px,最小字号8px防止过小
function calcLegendFont(container, baseFont = 12, baseWidth = 1920, minFont = 8) {
const width = container.clientWidth;
const scale = width / baseWidth;
return Math.max(minFont, Math.round(baseFont * scale));
}
// ========= 只初始化一次折线图,不再每次刷新销毁重建 =========
function initLineChart() {
const dom = document.getElementById('chartmain_zhe');
echarts3Instance = echarts.init(dom, null, { devicePixelRatio: window.devicePixelRatio });
const colors = ['#ff7f50', '#87cefa', '#da70d6', '#32cd32', '#6495ed', '#ff69b0', '#ba55d3', '#cd5c5c', '#ffa500', '#40e0d0', '#1e90ff', '#ff6347', '#7b68ee', '#d0648a', '#ffd700', '#6b8e23', '#4ea397', '#3cb371', '#b8860b'];
const legendFont = calcLegendFont(dom, 35, 1920);
// 图例方块也同步缩放,1920下itemWidth/itemHeight=15
const itemSize = calcLegendFont(dom, 35, 1920);
const option = {
title: { text: '' },
tooltip: {
trigger: 'axis',
confine: false, // 限制在图表内
backgroundColor: 'rgba(0,18,36,0.9)',
textStyle: { color: '#fff', fontSize: 14 },
padding: [12, 16],
borderWidth: 1,
borderColor: '#0efdff',
},
legend: {
// type: 'scroll', textStyle: { color: '#fff', fontSize: 12, lineHeight: 15, },
// // icon: 'circle',
// pageIcon: 'circle', pageIconColor: '#0efdff', pageIconSize: 12,
// pageIconInactiveColor: '#666', pageTextStyle: { color: '#0efdff' },
type: 'plain',
orient: 'horizontal', // ✅必须开启水平排列,超出width自动换行
left: 10,
right: 10,
top: 0,
width: 'auto', // 或者写固定像素,例如 width:450;left+right限制可用宽度
itemGap: 2, // 图例项水平间距
itemWidth: itemSize,
itemHeight: itemSize,
textStyle: { color: '#fff', fontSize: legendFont, fontWeight: 700 },
data: []
},
grid: { x: 30, y: 80, x2: 20, y2: 20 },
xAxis: {
type: 'category', boundaryGap: false, data: [],
axisLabel: { interval: 0, color: '#fff', fontSize: 12 },
axisTick: { show: false },
axisLine: { show: true, symbol: ['none', 'arrow'], symbolOffset: 12, lineStyle: { color: '#fff' } }
},
yAxis: {
type: 'value', name: '单位:个', minInterval: 1,
axisLine: { show: true, symbol: ['none', 'arrow'], symbolOffset: 12, lineStyle: { color: '#fff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 12 }
},
series: []
}
echarts3Instance.setOption(option);
// dom.addEventListener('mouseenter', stopLineTipLoop);
// dom.addEventListener('mouseleave', startLineTipLoop);
// startLineTipLoop();
}
// 仅更新折线数据,不销毁实例
function updateLineData(list) {
const xData = list[0].dayline.map(item => item.month + '月');
const legendData = list.map(item => item.groupname);
const colors = ['#D62728', '#FF7F0E', '#FFBB78', '#2CA02C', '#17BECF', '#1F77B4', '#9467BD', '#8C564B', '#E377C2', '#BCBD22', '#393B79', '#637939',
'#8C6D31', '#843C39', '#5254A3', '#BD9E39', '#3182BD', '#E6550D', '#7F7F7F'];
const series = list.map((item, i) => ({
name: item.groupname,
type: 'line',
symbol: 'circle',
symbolSize: 7,
// 无数据转为 null,线条自动断开
data: item.dayline.map(v => {
if (!v.sl || v.sl <= 0) {
return null
}
return v.sl
}),
connectNulls: false, // 遇到null不连线
itemStyle: { color: colors[i] },
lineStyle: { color: colors[i], width: 2 }
}))
echarts3Instance.setOption({
xAxis: { data: xData },
legend: { data: legendData },
series: series
})
}
// 窗口自适应
function handleResize() {
if (echarts1Instance) echarts1Instance.resize();
if (echarts3Instance) echarts3Instance.resize();
}
// 原生Ajax请求封装
function httpGet(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function () {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
callback(res);
}
}
xhr.onerror = function () {
console.log('接口请求失败:' + url);
}
xhr.send();
}
// 初始化数据
function init() {
renderNumList('sourceMonthWrap', sourceMonth);
renderNumList('sourceYearWrap', sourceYear);
// 一次性初始化两张图表,后续只更新数据
initBarChart();
initLineChart();
getDataInfo();
}
// 获取全部统计数据(30秒只更新数据,不重建图表)
//http://47.97.125.165:8905
function getDataInfo() {
const apiUrl = '/api/transitstat/getallcount';
httpGet(apiUrl, function (res) {
const data = res.data;
// 赋值当月/当年数字
sourceMonth.forEach(item => item.num = data.regStatAllMonth[item.pre]);
sourceYear.forEach(item => item.num = data.regStatAllYear[item.pre]);
listData = data.regStatHospMonth;
// 匹配流程数量&百分比
const baseNum = data.regStatStatusTotal['检验申请'] || 0
dayCounts.forEach(item => {
item.sl = res.data.regStatStatusTotal[item.dict_name] || 0
item.percent = (item.sl / baseNum * 100).toFixed(2) + '%'
if (item.percent.endsWith('.00%')) {
item.percent = item.percent.replace('.00%', '%')
}
})
const flowOrder = dayCounts.map(item => item.dict_name).filter(name => name !== '申请作废' && name !== '标本退回');
//初始化全部remain=0
dayCounts.forEach(d => d.remain = 0);
for (let i = 1; i < flowOrder.length; i++) {
const prevName = flowOrder[i - 1];
const currName = flowOrder[i];
const prevItem = dayCounts.find(x => x.dict_name === prevName);
const currItem = dayCounts.find(x => x.dict_name === currName);
if (prevItem && currItem) {
currItem.remain = prevItem.sl - currItem.sl;
if (currItem.remain < 0) currItem.remain = 0;
if (prevItem.sl > 0) {
currItem.remainPer = (currItem.remain / prevItem.sl * 100).toFixed(2) + '%';
} else {
currItem.remainPer = "0%";
}
}
}
// console.log('dayCounts==>', dayCounts);
// 刷新文字列表
renderNumList('sourceMonthWrap', sourceMonth);
renderNumList('sourceYearWrap', sourceYear);
renderDayCounts();
renderListData();
// 增量更新图表,不销毁画布(解决刷新卡顿)
updateBarData(data.regStatMonth);
updateLineData(data.regStatHospYear);
})
}
// setTimeout(function () {
// window.location.href = "index2.html";
// }, 60000);
let dataRefreshTimer = null;
// 页面初始化
window.onload = function () {
// 窗口/全屏监听
window.addEventListener('resize', handleResize);
document.addEventListener('fullscreenchange', handleResize);
document.addEventListener('webkitfullscreenchange', handleResize);
// 首次拉取数据
init();
// 定时刷新数据
dataRefreshTimer = setInterval(getDataInfo, 60000);
}
// 页面销毁释放资源
window.onbeforeunload = function () {
if (dataRefreshTimer) {
clearInterval(dataRefreshTimer);
dataRefreshTimer = null;
}
window.removeEventListener('resize', handleResize);
document.removeEventListener('fullscreenchange', handleResize);
document.removeEventListener('webkitfullscreenchange');
if (echarts1Instance) echarts1Instance.dispose();
if (echarts3Instance) echarts3Instance.dispose();
}
</script>
</body>
</html>