Files
his/openhis-ui-vue3/src/action/nurseStation/temperatureSheet/drawfn.js
zhaoyun 3bbffc47c1 fix(#566): 请修复 Bug #566:[一般] [住院护士站-三测单] 体征数据已录入成功,但在“体温单”图表区中未渲染显示数据点
根因:
- Bug #请修复 Bug #566 存在的问题

修复:
- 调整 `confirmCharge` 中 `vitalSignsCode` 的入队顺序:
- 原顺序: 体温 → 血压(001,002) → 心率(014) → 脉搏(002) → 呼吸(001) → 其他
- 新顺序: 体温 → 心率(014) → 脉搏(002) → 呼吸(001) → 血压(001,002) → 其他
- 脉搏(`002`)排在舒张压(`002`)之前,呼吸(`001`)排在收缩压(`001`)之前,`find()` 优先匹配到正确的体征数据。
- 2. `src/action/nurseStation/temperatureSheet/drawfn.js`**
- 问题**: 旧数据兼容层中 `some()` 检查会阻止添加映射编码。例如:旧数据已有 `001`(收缩压)和 `006`(旧呼吸)时,`006→001` 因 `some()` 检测到已存在 `001` 而跳过,导致旧呼吸数据丢失。
- 移除 `some()` 检查,始终添加映射条目
- 用 `unshift()` 替代 `push()`,将映射后的脉搏(`002`)、呼吸(`001`)条目插入 `rowBOS` 头部,确保 `find()` 优先匹配它们而非同编码的血压条目
2026-05-28 23:21:46 +08:00

702 lines
22 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 数据处理
import * as d3 from 'd3';
import {symbol} from 'd3-shape';
import {
degreesOnline,
disconnectEvents,
getBrokenLine,
getHeartRate,
getType,
getTypeAnimalHeat,
getTypeData,
getTypeDatas,
postpartumDays,
setMergeTag,
} from './utils';
import {BOTTOM_KEYS, HEAD_HEIGHT, LINE_HEIGHT, symbolArrowHeight, textLeftMargin, TOP_KEYS,} from './config';
export const iconDrawObj = {
// 绘制圆形图标
getDrawRoundIcon: ({ content, data, x, y, fill = 'blue', stroke = 'blue', r = 5 }) => {
// 增加icon 红色空心
content
.append('g')
.attr('fill', fill)
.attr('stroke', stroke)
.attr('stroke-width', 1)
.selectAll('circle')
.data(data)
.join('circle')
.attr('transform', (i) => {
const yVal = y(i) || 0;
if (!yVal) {
return 'scale(0)';
}
return '';
})
.attr('cx', x)
.attr('cy', y)
.attr('r', (i) => {
if (y(i)) {
return r;
}
return 0;
});
},
// 绘制圆形点图标
getDrawRoundDotIcon: ({
content,
data,
x,
y,
fill = 'white',
stroke = 'blue',
deepFill = 'blue',
r = 6,
}) => {
content
.append('g')
.attr('fill', fill)
.attr('stroke', stroke)
.attr('stroke-width', 1)
.selectAll('circle')
.data(data)
.join('circle')
.attr('transform', (i) => {
const yVal = y(i) || 0;
if (!yVal) {
return 'scale(0)';
}
return '';
})
.attr('cx', x)
.attr('cy', y)
.attr('r', r)
.clone()
.attr('cx', x)
.attr('cy', y)
.attr('r', 1)
.attr('fill', deepFill);
},
// 绘制x
getDrawXIcon: ({ content, data, x, y, fill = 'blue', stroke = 'blue' }) => {
content
.append('g')
.attr('fill', fill)
.attr('stroke', stroke)
.attr('stroke-width', 1)
.selectAll('line')
.data(data)
.join('line')
.attr('transform', (i) => {
let yVal = y;
if (typeof y === 'function') {
yVal = y(i) || 0;
}
if (!yVal) {
return 'scale(0)';
}
return '';
})
.attr('x1', function (d) {
return x(d) - 4;
})
.attr('y1', function (d) {
return y(d) - 4;
})
.attr('x2', function (d) {
return x(d) + 4;
})
.attr('y2', function (d) {
return y(d) + 4;
})
.clone()
.attr('x1', function (d) {
return x(d) + 4;
})
.attr('y1', function (d) {
return y(d) - 4;
})
.attr('x2', function (d) {
return x(d) - 4;
})
.attr('y2', function (d) {
return y(d) + 4;
});
},
// 绘制三角形
drawThreeIcon: ({ content, data, x, y, fill = 'blue', stroke = 'blue', riangle = 48 }) => {
// 蓝色三角形
content
.append('g')
.attr('class', 'ceshiname')
.selectAll('g')
.data(data)
.join('g')
.attr('transform', (i) => {
const yVal = y(i) || 0;
if (!yVal) {
return 'scale(0)';
}
return `translate(${x(i)},${yVal})`;
})
.append('path')
.call((path) => {
const symbolThree = symbol();
const symbolIndex = 5;
symbolThree.type(d3.symbols[symbolIndex]);
path.attr('d', symbolThree.size(riangle)).attr('fill', fill).attr('stroke', stroke);
// .transition()
// .duration(1500)
// .attr('d', symbolThree.size(48))
});
},
};
export function getG(svg, viewConfig) {
return svg
.append('g')
.attr('transform', `translate(${viewConfig.marginLeft},${viewConfig.marginTop + HEAD_HEIGHT})`);
}
// 设置数据
export function getData(allData) {
const rowsData = allData.rows; // allData, '【全部数据】'
// 兼容旧数据:将旧 typeCode 映射到新 typeCode心率 004→014脉搏 005→002呼吸 006→001
const OLD_CODE_MAP = { '004': '014', '005': '002', '006': '001' };
rowsData.forEach(row => {
if (row.rowBOS) {
const prependItems = [];
row.rowBOS.forEach(item => {
const newCode = OLD_CODE_MAP[item.typeCode];
// 始终添加映射条目,用 unshift 插入数组头部
// 这样 getType 的 find() 优先匹配映射后的编码(如脉冲、呼吸)
// 即使存在同编码的旧条目(如血压舒张压用 002、收缩压用 001
// 映射后的脉搏(002)和呼吸(001)条目排在前面,确保图表正确渲染
if (newCode) {
prependItems.push({ ...item, typeCode: newCode });
}
});
row.rowBOS.unshift(...prependItems);
}
});
const infoData = allData.grParamBOS;
const typesData = getTypeDatas(allData.types, allData.grParamBOS.beginDate);
const selectOp = allData.selectOp;
const symbolTextArr = getTypeData('018', rowsData, false, allData.grParamBOS.beginDate); // 【特殊标记】
const symbolGoUp = getType('003', rowsData, allData.grParamBOS.beginDate); // 不升
// 体温单特殊数据
const dicData = JSON.parse(window.localStorage.getItem('transDictCode'));
const signsManagementList = dicData
? dicData
.filter((item) => item.name === '95')[0]
.concept.sort((a, b) => {
return a.displayOrder - b.displayOrder;
})
: [];
const otherArr = [];
if (signsManagementList.length > 0) {
for (let j = 0; j < signsManagementList.length; j++) {
if ((signsManagementList[j].code !== '9502') & (signsManagementList[j].code !== '9503')) {
const otherArrItem = getTypeData(
signsManagementList[j].code,
rowsData,
false,
allData.grParamBOS.beginDate
);
otherArr.push(otherArrItem);
}
}
}
const surgeryNumDays = allData.types.filter((item) => item.typeCode === '031');
const surgeryArr = getTypeData('9502', rowsData, false, allData.grParamBOS.beginDate); // '【手术】'
const minSurgeryArr = getTypeData('9503', rowsData, false, allData.grParamBOS.beginDate); // '【小手术】'
const temArr = getTypeData('00301', rowsData, false, allData.grParamBOS.beginDate); // '【体温拒测等】'
// const symbolGoUp = getType('003', rowsData, allData.grParamBOS.beginDate) // symbolGoUp, '不升'
// 35度线上的内容
const symbolDegreesEvents = degreesOnline(symbolTextArr, selectOp, 0);
const symbolDegreesOnline = disconnectEvents(symbolTextArr, selectOp, 'isBeforeAfter', 0);
// 40~42线上的内容
const symbolTopOnline = degreesOnline(symbolTextArr, selectOp, 1);
const symbolTopDegreesOnline = disconnectEvents(symbolTextArr, selectOp, 'isBeforeAfter', 1);
const symbolContent = getTypeData('013', rowsData, false, allData.grParamBOS.beginDate); // symbolContent, '【标记内容】'
const mergeTag = setMergeTag(symbolTopOnline, symbolContent); // mergeTag, '【合并标记,标记内容】'
// 产后日数
infoData.postpartum = postpartumDays('031', typesData); // infoData, '产后日数'
// 写死的先
infoData.dateClosed = {
stopTime: true, // 控制结束日期
stopNumber: true, // 控制住院天数
};
// 折线
const brokenLineData = getBrokenLine(
'003',
rowsData,
symbolDegreesOnline,
symbolGoUp,
symbolTopDegreesOnline,
allData.grParamBOS.beginDate
);
// 模拟数据 体温
const datasetAnus = getTypeAnimalHeat('003', rowsData, '1', allData.grParamBOS.beginDate); // datasetAnus, '腋温【x】'
const bodyData = getTypeAnimalHeat('003', rowsData, '2', allData.grParamBOS.beginDate); // dbodyData, '口温'
// const datasetHeartrate = getTypeAnimalHeat('003', rowsData)
const datasetHeartrate = getTypeAnimalHeat('003', rowsData, '3', allData.grParamBOS.beginDate); // datasetHeartrate, '肛温【红空圆】'
const earCool = getTypeAnimalHeat('003', rowsData, '4', allData.grParamBOS.beginDate); // earCool, '耳朵温'
const painScore = getHeartRate(
// painScore, '疼痛'
'016',
rowsData,
symbolDegreesOnline,
symbolTopDegreesOnline,
true,
allData.grParamBOS.beginDate
);
const allTemperatureData = [bodyData, datasetAnus, datasetHeartrate, earCool]; // 所有的温度记录
// '脉搏【红实圆】'
const datasetPulse = getHeartRate(
'002',
rowsData,
symbolDegreesOnline,
symbolTopDegreesOnline,
true,
allData.grParamBOS.beginDate
);
// 心率【空心】
const datasetHeartRate = getHeartRate(
'014',
rowsData,
symbolDegreesOnline,
symbolTopDegreesOnline,
true,
allData.grParamBOS.beginDate
);
// const allTemperatureData = getDrawData('003', rowsData, '1', allData.grParamBOS.beginDate) // datasetAnus, '腋温【x】'
// const dataCool = getDrawCoolData('015', rowsData, true, allData.grParamBOS.beginDate) // dataCool, '【物理降温】'
const dataCool = getTypeData('015', rowsData, true, allData.grParamBOS.beginDate); // 【物理降温】
// 呼吸【黑实圆】
const datasetPain = getTypeData('001', rowsData, false, allData.grParamBOS.beginDate); // 呼吸
const title = infoData.title;
return {
title,
datasetHeartRate,
bodyData,
datasetAnus,
datasetHeartrate,
earCool,
painScore,
allTemperatureData,
datasetPulse,
symbolContent,
symbolTextArr,
mergeTag,
dataCool,
datasetPain,
typesData,
infoData,
rowsData,
symbolDegreesOnline,
symbolGoUp,
symbolDegreesEvents,
brokenLineData,
otherArr,
temArr,
surgeryArr,
minSurgeryArr,
surgeryNumDays,
};
}
export function drawTopMask(svg, viewConfig) {
// ===================== //
// 遮罩层挡住超出的折线 //
// =====================//
svg
.append('g')
.attr('transform', `translate(${viewConfig.marginLeft},${HEAD_HEIGHT - LINE_HEIGHT})`)
.append('rect')
.attr('class', 'mask-rect')
.attr('x', 0)
.attr('y', -100)
.attr('width', viewConfig.contentWidth)
// 2.5是上下的一个区域 1 是原来有 2的宽度
.attr('height', LINE_HEIGHT * (TOP_KEYS.length + 7.5) - 1)
.attr('stroke', viewConfig.stroke)
.attr('fill', '#fff')
.attr('style', 'stroke-width: 0');
drawTopVerticalLine(svg, viewConfig);
}
export function drawTopVerticalLine(svg, viewConfig) {
// 补上下竖线
let start = viewConfig.step;
const lineG = getG(svg, viewConfig).append('g').attr('class', 'maskline-top');
while (start < viewConfig.contentWidth) {
// const isLastLine = (start + viewConfig.step) >= viewConfig.contentWidth
lineG
.append('line')
.attr('fill', 'stroke')
.attr('x1', start)
.attr('y1', 0)
.attr('y2', (TOP_KEYS.length + 1) * LINE_HEIGHT)
.attr('x2', start)
.attr('stroke', start > viewConfig.step ? '#B22222' : viewConfig.stroke)
// .attr('stroke', isLastLine ? 'black' : (start > viewConfig.step ? '#B22222' : viewConfig.stroke))
.attr('stroke-width', 4); // 设置线条宽度为2
start = start + viewConfig.step;
}
}
export function drawBottomMask(svg, viewConfig) {
const g = getG(svg, viewConfig);
// 遮罩层挡住超出的折线
g.append('rect')
.attr('class', 'mask-rect')
.attr('x', 4)
.attr('y', viewConfig.bottomKeysPosStart + viewConfig.micoStep * 2)
.attr('width', viewConfig.contentWidth)
.attr('height', LINE_HEIGHT * (BOTTOM_KEYS.length + 5))
.attr('stroke', viewConfig.stroke)
.attr('fill', '#ffffff')
.attr('style', 'stroke-width: 0');
drawBottomMaskLine(svg, viewConfig);
}
function drawBottomMaskLine(svg, viewConfig) {
// 补上下竖线
let start = viewConfig.step;
const lineG = getG(svg, viewConfig).append('g').attr('class', 'maskline');
while (start < viewConfig.contentWidth) {
// const isLastLine = (start + viewConfig.step) >= viewConfig.contentWidth;
lineG
.append('line')
.attr('fill', 'stroke')
.attr('x1', start)
.attr('y1', viewConfig.bottomKeysPosStart)
.attr('y2', viewConfig.tableHeight)
.attr('x2', start)
.attr('stroke', start > viewConfig.step ? '#B22222' : viewConfig.stroke)
//.attr('stroke', isLastLine ? 'black' : (start > viewConfig.step ? '#B22222' : viewConfig.stroke))
.attr('stroke-width', 4); // 设置线条宽度为2
start = start + viewConfig.step;
}
}
// 绘制特殊事件文字 --- 有时间
export function drawSpecialText(svg, viewConfig, textData) {
const g = getG(svg, viewConfig);
g.append('g')
.selectAll('text')
.data(textData)
.join('text')
.attr('style', 'font-size:14px; fill: red;')
.attr('class', 'mytext')
.html((d, i) => {
const t = (d?.value || '').split(',');
let time = '';
if (t.length > 1) {
time =
t[0] + '于' + transNum(t[1].split(':')[0]) + '时' + transNum(t[1].split(':')[1]) + '分';
// time = t[0] + '㇑' + transNum(t[1].split(':')[0]) + '时' + transNum(t[1].split(':')[1]) + '分'
}
const texts = time.split('');
return texts
.map((text, i) => {
return `<tspan dx="${
i === 1 ? -14 : i === 0 ? 0 : Number(text) ? -8 : -14
}" dy="${20}">${text}</tspan>`;
})
.join('');
})
.attr('x', (d, i) => {
return viewConfig.step + d.index * viewConfig.micoStep + textLeftMargin;
})
.attr('y', () => {
return viewConfig.topKeysPos - textLeftMargin;
});
}
// 绘制特殊事件文字 --- 大手术小手术
export function drawSurgery(svg, viewConfig, textData) {
const g = getG(svg, viewConfig);
const textDatas = [];
if (textData.length > 0) {
for (let j = 0; j < textData.length; j++) {
let item = {};
if (textData[j].value) {
item = {
index: textData[j].index,
value: textData[j].value.split(',')[0],
date: textData[j].date,
};
} else {
item = textData[j];
}
textDatas.push(item);
}
}
g.append('g')
.selectAll('text')
.data(textDatas)
.join('text')
.attr('style', 'font-size:15px; fill: red;')
.attr('class', 'mytext')
.html((d, i) => {
const texts = (d?.value || '').split('');
return texts
.map((text, i) => {
return `<tspan dx="${
i === 1 ? -15 : i === 0 ? 0 : Number(text) ? -8 : -15
}" dy="${20}">${text}</tspan>`;
})
.join('');
})
.attr('x', (d, i) => {
return viewConfig.step + d.index * viewConfig.micoStep + textLeftMargin - 2;
})
.attr('y', () => {
return viewConfig.topKeysPos - textLeftMargin;
});
}
// 绘制特殊事件文字 --- 无时间
export function drawSpecialNoTimeText(svg, viewConfig, textData) {
const g = getG(svg, viewConfig);
const textDatas = [];
if (textData.length > 0) {
for (let j = 0; j < textData.length; j++) {
let item = {};
if (textData[j].value) {
item = {
index: textData[j].index,
value: textData[j].value.split(',')[0],
date: textData[j].date,
};
} else {
item = textData[j];
}
textDatas.push(item);
}
}
g.append('g')
.selectAll('text')
.data(textDatas)
.join('text')
.attr('style', 'font-size:15px; fill: red;')
.attr('class', 'mytext')
.html((d, i) => {
const texts = (d?.value || '').split('');
// let time = ''
// if (t.length > 1) {
// time = t[0] + '㇑' + transNum(t[1].split(':')[0]) + '时' + transNum(t[1].split(':')[1]) + '分'
// }
// const texts = time.split('')
return texts
.map((text, i) => {
return `<tspan dx="${i === 1 ? -15 : i === 0 ? 0 : Number(text) ? -8 : -15}" dy="${
i === 0 ? 20 : 80
}">${text}</tspan>`;
})
.join('');
})
.attr('x', (d, i) => {
return viewConfig.step + d.index * viewConfig.micoStep + textLeftMargin - 2;
})
.attr('y', () => {
return viewConfig.topKeysPos - textLeftMargin;
});
}
// 数字转换成文字
function transNum(num) {
const arr1 = ['', '十'];
const arr2 = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
const sw = num.split('')[0];
const gw = num.split('')[1];
const str1 = sw > 1 ? arr2[sw] + '十' : arr1[sw];
let strNum = str1 + arr2[gw];
// 若H为十点整小时则把零去掉若m为十分,则去掉零
if (strNum.indexOf('十') > 0 || strNum.indexOf('零') === 1) {
strNum = strNum.replace('零', '');
}
return strNum;
}
// 绘制标签文字
export function drawTagText(svg, viewConfig, textData) {
const g = getG(svg, viewConfig);
g.append('g')
.selectAll('text')
.data(textData)
.join('text')
.attr('style', 'font-size:14px; fill: black;')
.attr('class', 'mytext')
.html((d, i) => {
const texts = (d?.value || '').split('');
return texts
.map((text, i) => {
return `<tspan dx="${
i === 1 ? -14 : i === 0 ? 0 : Number(text) ? -8 : -14
}" dy="${20}">${text}</tspan>`;
})
.join('');
})
.attr('x', (d, i) => viewConfig.step + d.index * viewConfig.micoStep + textLeftMargin)
.attr('y', () => {
return viewConfig.topKeysPos - textLeftMargin + 198;
});
}
// 绘制特殊事件文字底部
export function drawBottomSpecialText(svg, viewConfig, textData) {
const g = getG(svg, viewConfig);
g.append('g')
.selectAll('text')
.data(textData)
.join('text')
.attr('style', 'font-size:14px; fill: blue;')
.attr('class', 'mytext')
.html((d, i) => {
const texts = (d?.value || '').split('');
return texts
.map((text, i) => {
return `<tspan dx="${
i === 1 ? -14 : i === 0 ? 0 : Number(text) ? -8 : -14
}" dy="${20}">${text}</tspan>`;
})
.join('');
})
.attr('x', (d, i) => viewConfig.step + d.index * viewConfig.micoStep + textLeftMargin)
.attr('y', (d) => {
const texts = (d?.value || '').split('');
return viewConfig.bottomKeysPosStart - texts.length * LINE_HEIGHT - 6 - 58;
});
}
export function initArrow(svg) {
const arrowMarkerRed = svg
.append('defs')
.append('marker')
.attr('id', 'redArrow')
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', '12')
.attr('markerHeight', '12')
.attr('viewBox', '0 0 12 12')
.attr('refX', '6')
.attr('refY', '6')
.attr('orient', 'auto');
const arrowPath = 'M2,2 L10,6 L2,10 L6,6 L2,2';
arrowMarkerRed.append('path').attr('d', arrowPath).attr('fill', 'red');
const arrowMarkerBlue = svg
.append('defs')
.append('marker')
.attr('id', 'blueArrow')
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', '12')
.attr('markerHeight', '12')
.attr('viewBox', '0 0 12 12')
.attr('refX', '6')
.attr('refY', '6')
.attr('orient', 'auto');
arrowMarkerBlue.append('path').attr('d', arrowPath).attr('fill', 'blue');
}
export function lineArrow({ svg, viewConfig, scale, data } = {}) {
const g = getG(svg, viewConfig);
const vaildData = data.filter((item) => item);
// {
// ...item,
// value: ismax ? maxDefault : minDefault,
// sourceValue: item,
// ismax: ismax ,
// max,
// min
// }
// 绘制直线
g.selectAll('line')
.data(vaildData)
.join('line')
.attr('x1', (d, i) => {
return scale(d.index) + +viewConfig.X_OFFSET;
})
.attr('y1', (d, i) => {
if (d.ismax) {
// 顶部箭头
return viewConfig.topKeysPos + symbolArrowHeight * 1.5;
} else {
// 底部箭头
return viewConfig.bottomKeysPosStart - symbolArrowHeight * 1.5;
}
})
.attr('x2', (d, i) => {
return scale(d.index) + +viewConfig.X_OFFSET;
})
.attr('y2', (d, i) => {
if (d.ismax) {
// 顶部箭头
return viewConfig.topKeysPos + symbolArrowHeight / 2;
} else {
// 底部箭头
return viewConfig.bottomKeysPosStart - symbolArrowHeight / 2;
}
})
.attr('stroke', (d) => {
return d.ismax ? 'red' : 'blue';
})
.attr('stroke-width', 2)
.attr('marker-end', (d, i) => {
return d.ismax ? 'url(#redArrow)' : 'url(#blueArrow)';
});
g.selectAll('text')
.data(vaildData)
.join('text')
.attr('class', 'desctextname')
.html((item, i) => {
const value = `${item.sourceValue}`.split('');
const dotIndex = value.indexOf('.');
return value
.map((d, index) => {
let y;
let multiple = index;
let addition = 0; // dot height
if (item.ismax) {
if (index >= dotIndex) {
addition = 8;
multiple = index - 1;
}
y =
viewConfig.topKeysPos +
symbolArrowHeight * 1.5 +
(2 + multiple * 1.5) * viewConfig.X_OFFSET +
addition;
} else {
if (index >= dotIndex) {
addition = 9;
multiple = index - 1;
}
y =
viewConfig.bottomKeysPosStart -
(2 + (value.length - multiple) * 1.5) * viewConfig.X_OFFSET +
addition;
}
return `
<tspan rotate="90" x="${scale(item.index) + viewConfig.X_OFFSET - 4}"
y="${y}">${d}</tspan>`;
})
.join('');
})
.attr('x', (item, i) => {
return scale(item.index) + viewConfig.X_OFFSET - 4;
})
.attr('y', (item) => {
if (item.ismax) {
return viewConfig.topKeysPos + symbolArrowHeight * 1.5 + LINE_HEIGHT / 2;
} else {
return viewConfig.bottomKeysPosStart - symbolArrowHeight / 2 - LINE_HEIGHT / 2;
}
})
.attr('style', (item) => {
return `font-size:14px;fill:${item.ismax ? 'red' : 'blue'};font-weight: bold;`;
});
}