先看页面效果
本地dicom文件归档
2.页面结构
我这里用的是二级路由
<template>
<div class="page">
<div class="header">操作栏</div>
<div class="body">
<div class="study">
<router-view></router-view>
</div>
<div class="operate">
<div class="navbar">
<div class="navlist-item" v-for="(item, index) in navbar" :key="index">{{ item.label }}</div>
</div>
<div class="series">
<series-bar></series-bar>
</div>
</div>
</div>
</div>
</template>
<script>
import seriesBar from "@/components/seriesBar/index"
export default {
name: "treatmentManagement",
components: {
seriesBar
},
data() {
return {
navbar: [
{
label: '预览',
path: '/treatmentManagement/dicomView'
},
{
label: '分割',
path: '/treatmentManagement/segment'
},
{
label: '调整',
path: '/treatmentManagement/adjust'
},
{
label: '计划',
path: '/treatmentManagement/plan'
}
],
};
},
};
</script>
<style scoped lang="scss">
.page {
width: 100%;
height: 100%;
.header {
width: 100%;
height: 50px;
}
.body {
height: calc(100% - 50px);
display: flex;
flex-direction: row;
align-items: center;
.study, .operate{
height: 100%;
}
.study{
width: 80%;
}
.operate{
width: 20%;
.navbar{
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.series{
height: calc(100% - 50px);
}
}
}
}
</style>
3.seriesBar
获取存在vuex中的影像(归档后的影像)
<script>
import seriesItem from "@/components/seriesItem/index.vue";
// import { useArchiveStore } from "@/store/archiveStore.js";
// const archiveStore = useArchiveStore();
import { mapState } from "vuex"
export default {
name: "series",
components: {
seriesItem,
},
data() {
return {
seriesItemRefs: [],
domWidth: 146,
seriesListData: [],
};
},
computed:{
...mapState('archiveStore', ['currentStudy']),
currentStudy(){
return this.$store.state.currentStudy
}
},
watch: {
"$store.state.archiveStore.currentStudy": {
handler(newValue, oldValue) {
let { seriesList } = newValue;
this.seriesListData = seriesList;
},
immediate: true,
},
},
methods: {
getItemRef(el, idx) {
if (el) {
this.seriesItemRefs[idx] = el;
}
},
onSelected({ pos }) {
for (let i = 0; i < this.seriesItemRefs.length; i++) {
if (i === pos) {
this.seriesItemRefs[i].setSelected(true);
} else {
this.seriesItemRefs[i].setSelected(false);
}
}
},
setWidth(width) {
this.domWidth = width;
},
},
};
</script>
<template>
<div class="seriesbar">
<series-item
v-for="(series, idx) in seriesListData"
:key="series.seriesInsUid"
:ref="(el) => getItemRef(el, idx)"
:series="series"
:pos="idx"
@selected="onSelected"
></series-item>
</div>
</template>
<style lang="scss" scoped>
.seriesbar {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
white-space: nowrap;
overflow-x: hidden;
overflow-y: auto;
padding: 2px 1px;
font-size: 1.2rem;
flex-shrink: 0;
}
</style>
3.seriesItem
主要在这显示归档的dicom影像
<script>
import { ref, computed, onMounted } from "vue";
import { toolGroup } from "@/utils/initTools";
import { desensitizeSubstring } from "@/utils/index.js";
export default {
name: "series",
props: {
series: {
type: Object,
required: true,
},
pos: {
type: Number,
required: true,
},
},
computed: {
seriesTitle() {
let patName = this.$props.series.study.patientName;
// patName = desensitizeSubstring(patName, 1, -1);脱敏
return (
this.$props.series.study.modality +
" - " +
this.$props.series.seriesNumber +
" " +
patName
);
},
thumbClass() {
let ss = "thumbnail";
let s = "unselect";
if (this.IsSel) {
s = "selected";
}
return ss + " " + s;
},
},
data() {
return {
IsSel: "",
imgSrc: "",
};
},
methods: {
onClick(e) {
this.$emit("selected", { pos: this.$props.pos });
},
setSelected(bSel) {
this.IsSel = bSel;
}
},
mounted() {
this.$props.series.GetThumb().then((png) => {
this.imgSrc = new URL(png, import.meta.url).href;
});
},
};
</script>
<template>
<!-- <a-tooltip placement="RightTop" color="#2db7f5">
</a-tooltip> -->
<div>
<div>
<!-- {{ title }}<br /> -->
{{ series.seriesDesc }}<br />
{{ series.seriesDate }}<br />
{{ series.seriesTime }}
</div>
<!-- <template #title>
<span>
{{ seriesTitle }}<br />
{{ series.seriesDesc }}<br />
{{ series.seriesDate }}<br />
{{ series.seriesTime }}
</span>
</template> -->
<div
:class="thumbClass"
draggable="true"
@click="onClick"
>
<h3 class="thumb-title">{{ seriesTitle }}</h3>
<div class="img-container">
<img class="img-contain" :src="imgSrc" alt="" draggable="false" />
</div>
<p class="serdesc">{{ series.seriesDesc }}</p>
<p class="serdate">{{ series.seriesDate }}</p>
<p class="sertime">{{ series.seriesTime }}</p>
<p class="imgcnt">{{ series.count }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.thumbnail {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
background-color: var(--color-study-bg);
border-radius: 4px;
border-top: 1px solid var(--color-border-lt);
border-left: 1px solid var(--color-border-lt);
border-right: 2px groove var(--color-border-rb);
border-bottom: 2px groove var(--color-border-rb);
flex-shrink: 0;
}
.thumbnail {
width: 100%;
aspect-ratio: 4/3;
}
.thumb-title {
height: 2rem;
font-weight: normal;
}
h3 {
cursor: default;
font-size: 1.4rem;
margin: 0;
}
.tooltip-thumb {
font-size: 1.6rem;
}
p {
position: absolute;
color: #12e08a;
font-size: 1.25rem;
&.serdesc {
width: 100%;
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
left: 0.2rem;
top: 2rem;
}
&.serdate {
left: 2px;
top: 3.9rem;
}
&.sertime {
left: 0.2rem;
top: 5.7rem;
}
&.imgcnt {
right: 0.6rem;
bottom: 0.6rem;
}
}
.img-container {
display: flex;
background-color: black;
width: 100%;
height: 7.5rem;
align-items: center;
justify-content: center;
flex: 1;
}
img {
width: auto;
height: 90%;
}
.selected {
background-color: var(--color-study-active);
color: var(--color-selected-text);
}
.unselect {
background-color: var(--color-study-bg);
color: var(--color-unselect-text);
}
</style>
4.router-view对应的页面
<template>
<div class="page">
<displayer-area></displayer-area>
</div>
</template>
<script>
import displayerArea from "@/components/displayerArea/index.vue"
export default {
name: "dicomView",
components: {
displayerArea
}
}
</script>
<style scoped lang="scss">
</style>
5.displayerArea
<template>
<div class="displayarea" :style="containerStyle">
<Displayer
v-for="(v, idx) in pageSize"
:key="idx"
:ref="(el) => getDispRef(el, idx)"
:pos="idx"
@selected="onSelected"
/>
</div>
</template>
<script>
import { mapState } from "vuex";
import Displayer from "@/components/displayer/index";
export default {
name: "displayerArea",
components: {
Displayer,
},
data() {
return {
};
},
computed: {
containerStyle() {
const repeat = (n, s) => {
let dst = "";
for (let i = 0; i < n; i++) {
dst += s + " ";
}
return dst;
};
let result = {
display: "gird",
"grid-template-columns": repeat(
this.$store.getters.dicomViewPlayout.row,
"1fr"
),
height: "100%",
};
return result;
},
pageSize() {
return this.$store.getters.dicomViewPageSize;
},
},
methods: {
onSelected: ({ pos }) => {},
},
mounted() {},
};
</script>
<style scoped lang="scss">
.displayarea {
width: 100%;
height: 100%;
display: grid;
grid-gap: 1px 1px;
background-color: black;
color: #fff;
}
</style>
6.displayer
<script>
export default {
name: "displayer",
data() {
return {
IsSel: false,
IsHover: false,
};
},
computed: {
borderClass() {
let s = "selected";
if (this.IsSel) {
s = "selected";
} else {
if (this.IsHover) {
s = "hovered";
} else {
s = "unselect";
}
}
return s;
},
},
};
</script>
<template>
<div class="displaybox" :class="borderClass">
<div class="displayer" ref="displayer"></div>
</div>
</template>
<style lang="scss" scoped>
.displaybox {
position: relative;
display: flex;
flex-direction: row;
background-color: black;
.scroll-right {
width: 20px;
}
}
.displayer {
flex: 1;
text-align: left;
cursor: default;
user-select: none;
$font-size: 14px;
@mixin orient($text-align: left) {
position: absolute;
color: white;
font-size: $font-size;
text-align: $text-align;
z-index: 10;
}
.orient_top {
@include orient(center);
top: 2px;
left: calc(50% - 30px);
width: 60px;
}
.orient_bottom {
@include orient(center);
bottom: 2px;
left: calc(50% - 30px);
width: 60px;
}
.orient_left {
@include orient();
top: calc(50% - 20px);
left: 2px;
}
.orient_right {
@include orient();
top: calc(50% - 20px);
right: 2px;
}
}
.selected {
border: 1px solid red;
}
.hovered {
border: 1px dashed yellow;
}
.unselect {
border: 1px solid #fff;
}
</style>
7.选择本地文件夹
对dicom文件进行归档,并存在vuex里

<template>
<el-dialog
class="my_dialog"
title="请确认患者信息"
:visible.sync="dialogVisible"
width="950px"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="cancelClick"
>
<div class="m-content">
<el-form ref="form" :model="params" label-width="100px">
<el-form-item label="患者姓名" prop="name">
<el-input v-model="params.name"></el-input>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="params.gender">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="params.birthday"
type="date"
placeholder="选择日期"
>
</el-date-picker>
</el-form-item>
<el-form-item label="dicom文件">
<el-button @click="loadFolder">选择文件</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</template>
<script>
import {mapSate, mapGetters, mapMutations, mapActions} from 'vuex'
export default {
data() {
return {
dialogVisible: false,
params: {}
};
},
methods: {
...mapActions('archiveStore',['archiveFile']),
//打开弹窗
handleOpen() {
this.dialogVisible = true;
},
//关闭弹窗
cancelClick() {
this.dialogVisible = false;
},
//选择文件夹
async loadFolder() {
const fileHandles = await this.openFolder();
if (Array.isArray(fileHandles)) {
fileHandles.forEach(async (fileHandle) => {
// const file = fileHandle.name;
const file = await fileHandle.getFile();
// await archiveFile(file);
await this.$store.dispatch('archiveStore/archiveFile', file)
this.$router.push("/treatmentManagement/dicomView")
});
}
},
//打开系统弹窗
async openFolder() {
try {
const handle = await showDirectoryPicker();
const fileHandles = [];
await this.enumFiles(handle, fileHandles);
return fileHandles;
} catch (err) {
return null;
}
},
//处理文件
async enumFiles(handle, fileHandles) {
try {
// 处理文件
if (handle.kind === "file") {
if (handle.name.toLowerCase().endsWith(".dcm")) {
fileHandles.push(handle);
}
return;
}
// 处理目录
const itr = await handle.values();
for await (const entry of itr) {
try {
await this.enumFiles(entry, fileHandles);
} catch (e) {
console.warn(`无法访问 ${entry.name}:`, e);
}
}
} catch (err) {
console.error(`处理 ${handle.name} 时出错:`, err);
}
},
},
};
</script>
<style lang="scss" scoped>
</style>
8.vuex: archiveStore.js
import DCMStudy from "@/pacs/DCMStudy.js"
import DCMSeries from "@/pacs/DCMSeries.js"
import DCMImage from "@/pacs/DCMImage.js"
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import * as cornerstone from "@cornerstonejs/core"
import dicomParser from "dicom-parser"
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser
export default {
namespaced: true,
state: {
archiveData: {
studyList: [],
mainStudyId: "",
orgId: "",
dataformat: ""
},
currentStudy: null,
},
mutations: {
setArchiveData(state, payload){
state.archiveData = Object.assign({}, state.archiveData, payload)
},
setCurrentStudy(state, payload) {
state.currentStudy = payload;
},
},
actions: {
async archiveFile(store, file) {
let imageId = "";
if (typeof file === "string") {
imageId = "wadouri:/apii/webcloms/static/2022144725/dcm_org/" + file
} else {
imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file)
}
const dcmImage = new DCMImage({ imageId });
await dcmImage.parse();
if (!store.state.archiveData.mainStudyId) {
store.commit('setArchiveData', {
dataformat: "StudyList",
mainStudyId: dcmImage.studyInsUid,
orgId: "",
studyList: []
})
}
const study = store.state.archiveData.studyList.find(s => s.studyInsUid === dcmImage.studyInsUid);
if (study) {
const series = study.seriesList.find(s => s.seriesInsUid === dcmImage.seriesInsUid);
if (series) {
series.AddImage(dcmImage);
} else {
const series = new DCMSeries({
study,
seriesInsUid: dcmImage.seriesInsUid,
seriesNumber: dcmImage.seriesNumber,
sereisDesc: dcmImage.sereisDesc,
seriesDate: dcmImage.seriesDate,
seriesTime: dcmImage.seriesTime
});
series.AddImage(dcmImage);
study.AddSeries(series);
}
} else {
const study = new DCMStudy({
patientId: dcmImage.patientId,
patientName: dcmImage.patientName,
studyAge: dcmImage.studyAge,
birthday: dcmImage.birthday,
gender: dcmImage.gender,
studyId: dcmImage.studyId,
studyInsUid: dcmImage.studyInsUid,
studyDate: dcmImage.studyDate,
modality: dcmImage.modality,
isMain: dcmImage.PatientID === store.state.archiveData.mainStudyId
});
const series = new DCMSeries({
study,
seriesNumber: dcmImage.seriesNumber,
seriesInsUid: dcmImage.seriesInsUid,
sereisDesc: dcmImage.seriesDesc,
seriesDate: dcmImage.seriesDate,
seriesTime: dcmImage.seriesTime
});
series.AddImage(dcmImage);
study.AddSeries(series);
store.state.archiveData.studyList.push(study);
store.commit('setCurrentStudy', study)
}
}
}
}
9.vuex: dicom.js
export default {
namespaced: true,
state: {
layout: {
col: 2,
row: 2
},
pageSize: 4
},
mutations: {
setLayout(state, { col, row }) {
state.col = col
state.row = row
}
},
actions: {
}
}
10.工具函数: pacs/DCMImage.js
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
// import config from "@/config/index.js"
import { RenderingEngine, getRenderingEngine, Enums, metaData } from "@cornerstonejs/core";
// import * as cornerstone from "@cornerstonejs/core"
import { readTagAsString, decodeChinese, formartDcmDate, trimLeft, desensitizeSubstring, formartDcmTime } from "@/utils/readImage.js"
// cornerstone.init()
// 注册图像加载器
import * as cornerstone from "@cornerstonejs/core";
import dicomParser from 'dicom-parser'
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'
cornerstone.init()
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
// cornerstoneWADOImageLoader.configure({
// beforeSend: function (xhr) {
// const apiKey = localStorage.getItem('token');
// if (apiKey) {
// xhr.setRequestHeader('token', apiKey);
// }
// }
// });
var config = {
maxWebWorkers: navigator.hardwareConcurrency || 1,//创建web worker的最大数量,默认为1
startWebWorkersOnDemand: true, //默认情况下在需要时才创建web worker,如果希望在项目初始化时创建可设置为:false
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,//默认情况下web worker 不会在启动时初始化图片解码器,如果希望开启设置为:true
}
},
};
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
cornerstone.imageLoader.registerImageLoader("wadouri", cornerstoneWADOImageLoader.imageLoader)
export default class DCMImage {
constructor(args) {
//用于归档的数据-start
this.series = null; // 所属序列
this.parsed = false; // 是否已解析
this.imageId = "";
this.studyInsUid = "";
this.seriesInsUid = "";
this.sopInsUid = "";
this.instanceNumber = 1;
this.width = 0;
this.height = 0;
this.defWW = 400;
this.defWL = 40;
this.thickness = 0;
this.patientId = "";
this.studyAge = "";
this.birthday = "";
this.gender = "";
this.modality = "";
this.sereisDate = "";
this.seriesTime = "";
this.seriesDesc = "";
//用于归档的数据-end
//图像信息-start
this.PatientName = ""; // 姓名
this.AgeAndSex = ""; // 年龄(或者生日)+性别
this.ImageComments = ""; // 图像说明
this.EchoNumbers = ""; // 回波组号
this.PatientID = ""; // 病人病号
this.SeriesNo = ""; // 序列号
this.SeriesDateTime = ""; // 序列日期时间
this.StudyDateTime = ""; // 检查日期时间
this.HospitalName = ""; // 医院名称
this.StudyDesc = ""; // 检查描述
this.SeriesDesc = ""; // 序列描述
this.PatientPosition = ""; // 患者位置
this.DeviceName = ""; // 设备名称
this.StudyDate = ""; // 检查日期
this.AcquisitionDate = ""; // 采集日期
this.AcquisitionTime = ""; // 采集时间
this.ContentDate = ""; // 内容日期
this.ContentTime = ""; // 内容时间
this.Manufacturer = ""; // 制造商
this.Position = ""; // 方位 0x200B, 0x1011
this.ThicknessLocation = ""; // 厚度位置
this.Exposure = ""; // 曝光参数
this.TrTe = ""; // 拍片参数
this.AcquisitionDuration = ""; // 采集时长
//图像信息-end
if (args) {
this.imageId = args.imageId;
}
}
async parse() {
// const image = await cornerstoneDICOMImageLoader.wadouri.loadImage(
// this.imageId
// ).promise;
await cornerstone.imageLoader.loadImage(this.imageId).then(image => {
this.sopInsUid = image.data.string("x00080018"); // SOP Instance UID
this.studyInsUid = image.data.string("x0020000d"); // Study Instance UID
this.seriesInsUid = image.data.string("x0020000e"); // Series Instance UID
this.instanceNumber = image.data.string("x00200013"); // Instance Number
this.seriesDesc = image.data.string("x0008103E"); // Series Description
this.seriesDate = image.data.string("x00080021"); // Series Date
this.seriesTime = image.data.string("x00080031"); // Series Time
this.PatientPosition = image.data.string("x00180015"); // Body Part Examined
this.width = image.width;
this.height = image.height;
this.defWL = image.windowCenter;
this.defWW = image.windowWidth;
let val = image.data.string("x00180050");
if (val) this.thickness = parseFloat(val);
this.parsed = true;
//解析图像信息
this.readCornerText(image)
});
}
readCornerText(image) {
const lang = "zh_CN";
let charset = image.data.string("x00080005") || "ISO_IR 100";
const isUtf8 = charset.indexOf("ISO_IR 192") != -1;
const isCN = lang === "zh_CN";
let val;
let tag;
// 姓名
if (this.series && this.series.study && this.series.study.patientName) {
val = this.series.study.patientName;
} else {
tag = image.data.elements["x00100010"]; // 医院名称
if (tag) {
val = readTagAsString(
image.data.byteArray,
tag.dataOffset,
tag.length
);
}
if (val) {
val = decodeChinese(val, isUtf8);
}
}
if (config.desensitize) {
val = desensitizeSubstring(val, 1, -1);
}
this.PatientName = val;
val = image.data.string("x00101010"); // 年龄(或者生日)+性别
if (!val || val.length <= 0) {
val = image.data.string("x00100030");
if (!val) val = "";
} else {
if (isCN) {
val = val.replace(/Y|y/, "岁");
val = val.replace(/M|m/, "月");
val = val.replace(/D|d/, "天");
}
}
let tmp = image.data.string("x00100040");
if (!tmp || tmp.length <= 0) tmp = isCN ? "未知" : "UNKNOWN";
if (isCN) {
if (tmp === "M" || tmp === "m") tmp = "男";
else if (tmp === "F" || tmp === "f") tmp = "女";
else tmp = "未知";
}
val += " " + tmp;
this.AgeAndSex = val.replace(/^0+/g, "");
val = image.data.string("x00204000"); // 图像说明
if (val) {
this.ImageComments = decodeChinese(val, isUtf8);
}
val = image.data.string("x00180086"); // 回波组号
if (val) this.EchoNumbers = val;
this.PatientID = image.data.string("x00100020"); // 病人病号
val = image.data.string("x00200011"); // 序列号
if (val) this.SeriesNo = "Se:" + val;
// 序列日期时间
val = "";
if (this.series && this.series.seriesDate) val = this.series.seriesDate;
else val = image.data.string("x00080021"); // 序列日期
if (val) {
this.SeriesDateTime = formartDcmDate(val);
}
val = "";
if (this.series && this.series.seriesTime) val = this.series.seriesTime;
else val = image.data.string("x00080031"); // 序列时间
if (val) {
this.SeriesDateTime += " " + formartDcmTime(val);
}
if (this.series && this.series.study && this.series.study.studyDate)
this.StudyDateTime = this.series.study.studyDate; // 检查日期时间
else {
val = "";
val = image.data.string("x00080020"); // 检查日期
if (val) {
this.StudyDateTime = formartDcmDate(val);
}
val = "";
val = image.data.string("x00080030"); // 检查时间
if (val) {
this.StudyDateTime += " " + formartDcmTime(val);
}
}
val = "";
tag = image.data.elements["x00080080"]; // 医院名称
if (tag) {
val = readTagAsString(
image.data.byteArray,
tag.dataOffset,
tag.length
);
}
if (val) {
val = decodeChinese(val, isUtf8);
if (config.desensitize) {
val = desensitizeSubstring(val, 3, val.length - 2);
}
this.HospitalName = val;
}
val = "";
tag = image.data.elements["x00081030"]; // 检查描述
if (tag) {
val = readTagAsString(
image.data.byteArray,
tag.dataOffset,
tag.length
);
}
if (val) {
this.StudyDesc = decodeChinese(val, isUtf8);
this.StudyDesc = trimLeft(this.StudyDesc);
}
val = "";
tag = image.data.elements["x0008103e"]; // 序列描述
if (tag) {
val = readTagAsString(
image.data.byteArray,
tag.dataOffset,
tag.length
);
}
if (val) {
this.SeriesDesc = decodeChinese(val, isUtf8);
this.SeriesDesc = trimLeft(this.SeriesDesc);
}
val = image.data.string("x00185100"); // 患者位置
if (val) this.PatientPosition = val;
if (this.series && this.series.study && this.series.study.modality)
this.DeviceName = this.series.study.modality; // 设备名称
else this.DeviceName = image.data.string("x00080060"); // 设备名称 x00080060 Device Name
if (this.series && this.series.study && this.series.study.studyDate) {
this.StudyDate = this.series.study.studyDate; // 检查日期
} else {
val = image.data.string("x00080020"); // 检查日期 x00080020 Study Date
this.StudyDate = formartDcmDate(val);
}
val = image.data.string("x00080022"); // 接收日期
if (val) {
this.AcquisitionDate = formartDcmDate(val);
}
val = image.data.string("x00080032"); // TA
if (val) {
this.AcquisitionTime = formartDcmTime(val);
}
val = image.data.string("x00080023"); // 内容日期
if (val) {
this.ContentDate = formartDcmDate(val);
}
val = image.data.string("x00080033"); // 内容时间
if (val) {
this.ContentTime = formartDcmTime(val);
}
val = "";
tag = image.data.elements["x00080070"]; // 检查描述
if (tag) {
val = readTagAsString(
image.data.byteArray,
tag.dataOffset,
tag.length
);
}
if (val) {
this.Manufacturer = decodeChinese(val, isUtf8);
}
val = image.data.string("x200B1011"); // 方位 0x200B, 0x1011
if (val) this.Position = val;
val = image.data.string("x00180050"); // 厚度位置
if (val) {
val = "T:" + parseFloat(val).toFixed(1);
}
tmp = image.data.string("x00200032", 2);
if (tmp) {
val += " L:" + parseFloat(tmp).toFixed(1);
}
this.ThicknessLocation = val;
val = "";
let current = image.data.string("x00181151"); // 曝光参数
if (current) {
if (current.indexOf(".") !== -1)
current = current.substring(0, current.indexOf("."));
}
if (current && current.length > 0) {
current += "mA";
val += current;
}
let kvp = image.data.string("x00180060"); // 电压值
if (kvp) {
if (kvp.indexOf(".") !== -1)
kvp = kvp.substring(0, kvp.indexOf("."));
}
if (kvp && kvp.length > 0) {
kvp += "kV";
val += " " + kvp;
}
let exposuretime = image.data.string("x00181150");
if (exposuretime) {
if (exposuretime.indexOf(".") !== -1)
exposuretime = exposuretime.substring(
0,
exposuretime.indexOf(".")
);
}
if (exposuretime && exposuretime.length > 0) {
exposuretime += "ms";
val += " " + exposuretime;
}
let exposure = image.data.string("x00181152");
if (exposure) {
if (exposure.indexOf(".") !== -1)
exposure = exposure.substring(0, exposure.indexOf("."));
}
if (exposure && exposure.length > 0) {
exposure += "mAs";
val += " " + exposure;
}
this.Exposure = val;
let tr = image.data.string("x00180080"); // 重复时间
let te = image.data.string("x00180081"); // 回响时间
if (tr && te) {
// 拍片参数
this.TrTe = "TR:" + tr + " TE:" + te;
}
val = image.data.string("x00189073"); // 采集时长AD
if (val) this.AcquisitionDuration = "AD:" + val;
}
createThumb(type, size = 100) {
return new Promise(async (res, rej) => {
const elDiv = document.createElement("div");
let divW = this.width || 100;
let divH = this.height || 100;
let ratio = divW / divH;
if (size != 0) {
if (this.width > size) {
divW = size;
divH = divW / ratio;
} else if (this.height > size) {
divH = size;
divW = divH * ratio;
}
}
// 确保最小尺寸
divW = Math.max(1, divW);
divH = Math.max(1, divH);
elDiv.style.width = divW + "px";
elDiv.style.height = divH + "px";
elDiv.style.backgroundColor = "black";
elDiv.style.position = 'absolute'; // 添加绝对定位
elDiv.style.visibility = 'hidden'; // 初始隐藏
document.body.appendChild(elDiv);
requestAnimationFrame(async () => {
const renderingEngineId = "thumbRenderingEngine";
let renderingEngine = getRenderingEngine(renderingEngineId);
if (!renderingEngine) {
renderingEngine = new RenderingEngine(renderingEngineId);
}
const viewportId = "THUMBNAIL_STACK_" + this.sopInsUid;
const ViewportType = Enums.ViewportType;
const viewportInput = {
viewportId,
type: ViewportType.STACK,
element: elDiv,
defaultOptions: {
background: [0, 0, 0]
}
};
renderingEngine.enableElement(viewportInput);
let viewport = renderingEngine.getViewport(viewportId);
const stack = [this.imageId];
await viewport.setStack(stack);
viewport.render();
// 使用requestAnimationFrame确保渲染完成
await new Promise(resolve => requestAnimationFrame(resolve));
setTimeout(() => {
const cvs = elDiv.querySelector("canvas");
if (type === "png") {
const imgUri = cvs.toDataURL("image/png");
elDiv.remove();
res(imgUri);
} else if (type === "jpg") {
const imgUri = cvs.toDataURL("image/jpeg");
elDiv.remove();
res(imgUri);
}
}, 100)
})
});
}
}
11.工具函数: pacs/DCMSeries.js
export default class DCMSeries {
constructor(args) {
this.imageList = [];
this.study = null;
this.seriesInsUid = "";
this.seriesNumber = "";
this.sereisDesc = "";
this.seriesDate = null;
this.seriesTime = null;
this.thumb = null;
this.count = 0
if (args) {
this.study = args.study;
this.seriesInsUid = args.seriesInsUid;
this.seriesNumber = args.seriesNumber;
this.sereisDesc = args.sereisDesc;
this.seriesDate = args.seriesDate;
this.seriesTime = args.seriesTime;
}
}
AddImage(img, sort = false) {
if (!img) return;
const existImg = this.imageList.find(item => item.imageId === img.imageId);
if (!existImg) {
let { series, ...args } = img
args.createThumb = img.createThumb
this.imageList.push(args);
this.count = this.imageList.length
img.series = this;
if (sort) {
this.Sort();
}
}
}
Sort(bAsc = true) {
this.imageList.sort(function (a, b) {
const s1 = parseInt(a.instanceNumber);
const s2 = parseInt(b.instanceNumber);
return bAsc ? s1 - s2 : s2 - s1;
});
}
async GetThumb() {
if (this.thumb) {
return this.thumb;
} else {
if (this.imageList.length > 0) {
const img = this.imageList[0];
this.thumb = await img.createThumb("png");
return this.thumb;
} else {
return null;
}
}
}
GetImageByIndex(index){
return this.imageList[index]
}
GetImageById(imageId){
let result = this.imageList.find(item => item.imageId == imageId)
if(result) return result
}
GetImageIds(){
return this.imageList.map(item => item.imageId)
}
GetCount(){
return this.imageList.length
}
}
12,工具函数 pacs/DCMStudy.js
export default class DCMStudy {
constructor(args) {
this.seriesList = []; // 序列列表
this.patientId = "";
this.patientName = "";
this.studyAge = "";
this.birthday = "";
this.gender = "";
this.studyId = "";
this.studyInsUid = "";
this.studyDate = "";
this.modality = "";
this.isMain = false;
if (args) {
this.patientId = args.patientId;
this.patientName = args.patientName;
this.studyAge = args.studyAge;
this.birthday = args.birthday;
this.gender = args.gender;
this.studyId = args.studyId;
this.studyInsUid = args.studyInsUid;
this.studyDate = args.studyDate;
this.modality = args.modality;
this.isMain = args.isMain;
}
}
AddSeries(series, sort = false) {
if (!series) return;
const existSer = this.seriesList.find(item => item.seriesInsUid === series.seriesInsUid);
if (!existSer) {
// series.study = this;
this.seriesList.push(series);
let { seriesList, ...agrs } = this
series.study = agrs
if (sort) {
this.Sort();
}
}
}
Sort(bAsc = true) {
this.seriesList.sort(function (a, b) {
// 按序列号排序
const s1 = parseInt(a.seriesNumber);
const s2 = parseInt(b.seriesNumber);
return bAsc ? s1 - s2 : s2 - s1;
});
}
GetCount(){
console.log('获取长度', this.seriesList)
}
}
13.utils/readImage.js
function readTagAsString(byteArray, offset, length) {
// Create a view of the relevant portion of the array
const slice = byteArray.slice(offset, offset + length);
// Convert to string (assuming ISO-8859-1/Latin1 encoding)
return String.fromCharCode.apply(null, slice);
}
function decodeChinese(value, isUtf8) {
if (!value) return value;
if (isUtf8) {
// If UTF-8 encoded, decode using TextDecoder
const encoder = new TextDecoder('utf-8');
const bytes = new Uint8Array(value.split('').map(c => c.charCodeAt(0)));
return encoder.decode(bytes);
} else {
// For GBK/GB18030 encoding (common for Chinese DICOM files)
// Note: This requires a GBK decoder library or browser support
try {
// Modern browsers can handle GB18030
const encoder = new TextDecoder('gb18030');
const bytes = new Uint8Array(value.split('').map(c => c.charCodeAt(0)));
return encoder.decode(bytes);
} catch (e) {
// Fallback to Latin1 if decoding fails
return value;
}
}
}
function formartDcmDate(dateStr) {
if (!dateStr || dateStr.length < 8) return dateStr;
return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`;
}
function formartDcmTime(timeStr) {
if (!timeStr) return timeStr;
// Handle various time formats
const hours = timeStr.substring(0, 2) || '00';
const mins = timeStr.length >= 4 ? timeStr.substring(2, 4) : '00';
const secs = timeStr.length >= 6 ? timeStr.substring(4, 6) : '00';
return `${hours}:${mins}:${secs}`;
}
function trimLeft(str) {
return str ? str.replace(/^\s+/, '') : str;
}
function desensitizeSubstring(str, start, end) {
if (end == -1) {
end = str.length;
}
let len = end - start;
if (len > 5) len = 5;
let desenStr = str.substr(0, start) + "*".repeat(len) + str.substr(end);
return desenStr;
}
export {
readTagAsString, decodeChinese, formartDcmDate, trimLeft, desensitizeSubstring, formartDcmTime
}