Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing Details

uat_us
caiyiling 2024-05-24 17:15:13 +08:00
commit 29dbe52991
10 changed files with 1126 additions and 160 deletions

View File

@ -15,3 +15,28 @@ export function getSubjectImageUploadList(params) {
params params
}) })
} }
// 预上传
export function preArchiveDicomStudy(data) {
return request({
url: '/DownloadAndUpload/preArchiveDicomStudy',
method: 'post',
data
})
}
// 归档
export function addOrUpdateArchiveTaskStudy(data) {
return request({
url: '/DownloadAndUpload/addOrUpdateArchiveTaskStudy',
method: 'post',
data
})
}
// 删除上传文件
export function deleteTaskStudy(params) {
return request({
url: '/DownloadAndUpload/deleteTaskStudy',
method: 'delete',
params
})
}

View File

@ -1,6 +1,16 @@
<template> <template>
<div class="base-model-wrapper"> <div class="base-model-wrapper">
<el-dialog v-if="config.visible" v-dialogDrag :title="config.title" :append-to-body="config.appendToBody" :visible.sync="config.visible" :close-on-click-modal="false" :show-close="config.showClose" :width="config.width" :fullscreen="config.fullscreen"> <el-dialog
v-if="config.visible"
:v-dialogDrag="!config.dialogDrag"
:title="config.title"
:append-to-body="config.appendToBody"
:visible.sync="config.visible"
:close-on-click-modal="false"
:show-close="config.showClose"
:width="config.width"
:fullscreen="config.fullscreen"
>
<div class="base-modal-body"> <div class="base-modal-body">
<slot name="dialog-body" /> <slot name="dialog-body" />
</div> </div>
@ -12,24 +22,25 @@
</template> </template>
<script> <script>
export default { export default {
name: 'BaseDialog', name: "BaseDialog",
props: { props: {
config: { config: {
type: Object, type: Object,
default: () => { default: () => {
return { return {
visible: false, visible: false,
title: '', title: "",
closeOnClickModal: false, closeOnClickModal: false,
showClose: true, showClose: true,
appendToBody: false, appendToBody: false,
width: '100px', width: "100px",
fullscreen: false fullscreen: false,
} dialogDrag: false,
} };
} },
} },
} },
};
</script> </script>
<style lang="scss"> <style lang="scss">
.base-model-wrapper { .base-model-wrapper {
@ -54,4 +65,9 @@ export default {
padding: 10px; padding: 10px;
} }
} }
.notFooter {
.is-fullscreen .el-dialog__footer {
display: none;
}
}
</style> </style>

View File

@ -1,12 +1,107 @@
<template> <template>
<base-model :config="model_cfg"> <el-dialog
<div slot="dialog-body"> :visible.sync="visible"
<div class="top" style="margin-bottom: 10px"> :fullscreen="true"
<el-button type="primary" size="small" class="el-icon-upload2"> :close-on-click-modal="false"
custom-class="upload-dialog"
:before-close="beforeClose"
>
<span slot="title">{{ $t("trials:uploadImage:title:uploadImages") }}</span>
<el-divider content-position="left">{{
$t("trials:uploadImage:button:uploadTableTitle1")
}}</el-divider>
<!--已上传影像检查-->
<el-table :data="list" style="width: 100%" height="300" :loading="loading">
<!--受试者-->
<el-table-column
prop="SubjectCode"
:label="$t('trials:uploadImage:table:subjectCode')"
/>
<!--任务名称-->
<el-table-column
prop="TaskBlindName"
:label="$t('trials:uploadImage:table:taskBlindName')"
/>
<!--原始检查数-->
<el-table-column
prop="OrginalStudyList"
:label="$t('trials:uploadImage:table:orginalStudyListNum')"
>
<template slot-scope="scope">
<el-button
v-if="
scope.row.OrginalStudyList &&
scope.row.OrginalStudyList.length >= 1
"
type="text"
@click="handleOpenDialog(scope.row, 'study')"
>
<span>{{ scope.row.OrginalStudyList.length }}</span>
</el-button>
<span v-else>0</span>
</template>
</el-table-column>
<!--后处理检查数-->
<el-table-column
prop="UploadStudyList"
:label="$t('trials:uploadImage:table:uploadStudyListNum')"
>
<template slot-scope="scope">{{
scope.row.UploadStudyList && Array.isArray(scope.row.UploadStudyList)
? scope.row.UploadStudyList.length
: 0
}}</template>
</el-table-column>
<el-table-column
:label="$t('common:action:action')"
fixed="right"
width="140"
>
<template slot-scope="scope">
<!--下载--->
<el-button
circle
icon="el-icon-download"
:title="$t('trials:uploadImage:button:download')"
@click.stop="downloadImage(scope.row)"
/>
<!--删除--->
<el-button
circle
icon="el-icon-delete"
:title="$t('trials:uploadImage:button:delete')"
@click.stop="remove(scope.row)"
/>
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">{{
$t("trials:uploadImage:button:uploadTableTitle")
}}</el-divider>
<div
class="top"
style="margin-bottom: 10px"
v-if="Criterion.ImageUploadEnum > 0"
>
<el-button
type="primary"
size="small"
class="el-icon-upload2"
v-if="!isLoad"
>
<label for="file"> <label for="file">
{{ $t("trials:uploadImage:button:selectFolder") }} {{ $t("trials:uploadImage:button:selectFolder") }}
</label> </label>
</el-button> </el-button>
<el-button
type="primary"
size="small"
class="el-icon-upload2"
@click.stop="notUp"
v-else
>
{{ $t("trials:uploadImage:button:selectFolder") }}
</el-button>
<input <input
type="file" type="file"
name="file" name="file"
@ -18,59 +113,164 @@
@change="beginScanFiles($event)" @change="beginScanFiles($event)"
/> />
</div> </div>
<!--本地影像检查-->
<el-table <el-table
:data="list" :data="uploadList"
border
style="width: 100%" style="width: 100%"
height="300" v-adaptive="{ bottomOffset: 50 }"
:loading="loading"
> >
<!--受试者--> <el-table-column type="index" width="40" />
<el-table-column <!--检查信息-->
prop="subjectCode" <el-table-column min-width="200" show-overflow-tooltip>
:label="$t('trials:uploadImage:table:subjectCode')" <template slot="header">
/> <el-tooltip placement="top">
<!--任务名称--> <div slot="content">
<el-table-column {{ $t("trials:uploadImage:table:studyDetail1") }}<br />
prop="taskBlindName" {{ $t("trials:uploadImage:table:studyDetail2") }}<br />
:label="$t('trials:uploadImage:table:taskBlindName')" {{ $t("trials:uploadedDicoms:table:studyDate") }}
/> </div>
<!--原始检查数--> <span>{{ $t("trials:uploadImage:table:studyInfo") }}</span>
<el-table-column </el-tooltip>
prop="orginalStudyList" </template>
:label="$t('trials:uploadImage:table:orginalStudyListNum')" <template slot-scope="scope">
<div style="line-height: 15px">
<div>
<div>
<span>
<span style="font-weight: 500">Acc:</span>
<span
:class="{
colorOfRed: !scope.row.dicomInfo.AccessionNumber,
}"
>{{ scope.row.dicomInfo.AccessionNumber || "N/A" }}</span
> >
<template slot-scope="scope">{{ </span>
scope.row.orginalStudyList.length </div>
}}</template> <div style="display: inline-block; margin-right: 2px">
<span v-if="scope.row.dicomInfo.Modalities.length > 0">
{{ scope.row.dicomInfo.Modalities.join("、") }},
</span>
<span v-else style="color: #f44336">N/A,</span>
</div>
<div style="display: inline-block; margin-right: 2px">
<span v-if="scope.row.SeriesInstanceUidList.length"
>{{ scope.row.SeriesInstanceUidList.length }} Series,
</span>
<span v-else style="color: #f44336">N/A,</span>
</div>
<div style="display: inline-block">
<span v-if="scope.row.count"
>{{ scope.row.count }} Instances
</span>
<span v-else style="color: #f44336">N/A</span>
</div>
</div>
<div>
<div style="display: inline-block; margin-right: 2px">
<span v-if="scope.row.dicomInfo.seriesBodyPartExamined">
{{ scope.row.dicomInfo.seriesBodyPartExamined }},
</span>
<span v-else style="color: #f44336">N/A, </span>
</div>
<div style="display: inline-block">
<span v-if="scope.row.dicomInfo.StudyDescription">
{{ scope.row.dicomInfo.StudyDescription }}</span
>
<span v-else style="color: #f44336">N/A</span>
</div>
</div>
<div>
{{ scope.row.dicomInfo.StudyTime }}
</div>
</div>
</template>
</el-table-column> </el-table-column>
<!--后处理检查数--> <!--患者信息-->
<el-table-column <el-table-column min-width="130" show-overflow-tooltip>
prop="uploadStudyList" <template slot="header">
:label="$t('trials:uploadImage:table:uploadStudyListNum')" <el-tooltip placement="top">
min-width="140" <div slot="content">
{{ $t("trials:uploadImage:table:pId") }}<br />
{{ $t("trials:uploadImage:table:patientName") }}<br />
{{ $t("trials:uploadImage:table:pInfo") }}
</div>
<span>{{ $t("trials:uploadImage:table:patientInfo") }}</span>
</el-tooltip>
</template>
<template slot-scope="scope">
<div style="line-height: 15px">
<div>
<span v-if="scope.row.dicomInfo.PatientId"
><span style="font-weight: 500">PID: </span
>{{ scope.row.dicomInfo.PatientId }}</span
> >
<template slot-scope="scope">{{ <span v-else style="color: #f44336">N/A</span>
scope.row.orginalStudyList.length </div>
}}</template> <div>
<span
:class="[scope.row.dicomInfo.PatientName ? '' : 'colorOfRed']"
>
{{ scope.row.dicomInfo.PatientName || "N/A" }}
</span>
</div>
<div>
<span
:class="[scope.row.dicomInfo.PatientSex ? '' : 'colorOfRed']"
>
{{ scope.row.dicomInfo.PatientSex || "N/A" }},
</span>
<span
:class="[scope.row.dicomInfo.PatientAge ? '' : 'colorOfRed']"
>
{{ scope.row.dicomInfo.PatientAge || "N/A" }},
</span>
<span
:class="[
scope.row.dicomInfo.PatientBirthDate ? '' : 'colorOfRed',
]"
>
{{ scope.row.dicomInfo.PatientBirthDate || "N/A" }}
</span>
</div>
</div>
</template>
</el-table-column> </el-table-column>
<!--上传进度--> <!--上传进度-->
<el-table-column <el-table-column
prop="schedule" prop="schedule"
:label="$t('trials:uploadImage:table:schedule')" :label="$t('trials:uploadImage:table:schedule')"
min-width="150"
> >
<template slot-scope="scope">{{ <template slot-scope="scope">
scope.row.orginalStudyList.length <el-progress
}}</template> color="#409eff"
:percentage="scope.row.schedule"
v-if="scope.row.schedule || scope.row.schedule === 0"
/>
</template>
</el-table-column> </el-table-column>
<!--上传状态--> <!--上传状态-->
<el-table-column <el-table-column
prop="status" prop="status"
:label="$t('trials:uploadImage:table:status')" :label="$t('trials:uploadImage:table:status')"
min-width="120"
> >
<template slot-scope="scope">{{ <template slot-scope="scope">
scope.row.orginalStudyList.length <el-button
}}</template> :type="
['warning', 'success', 'danger', 'primary', 'success', 'danger'][
scope.row.status
]
"
size="mini"
plain
v-if="scope.row.status || scope.row.status === 0"
>{{ $fd("UploadStatus", scope.row.status) }}</el-button
>
</template>
</el-table-column> </el-table-column>
<!--已有/成功/失败--> <!--已有/成功/失败-->
<el-table-column <el-table-column
@ -78,27 +278,33 @@
:label="$t('trials:uploadImage:table:scheduleNum')" :label="$t('trials:uploadImage:table:scheduleNum')"
min-width="140" min-width="140"
> >
<template slot-scope="scope">{{
scope.row.orginalStudyList.length
}}</template>
</el-table-column>
<el-table-column :label="$t('common:action:action')" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <span v-if="scope.row.count">{{
circle `${scope.row.count} /${scope.row.successCount} / ${scope.row.failCount}`
icon="el-icon-delete" }}</span>
:title="$t('trials:uploadImage:button:delete')"
@click.stop="remove(scope.row, item)"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
:label="$t('common:action:action')"
fixed="right"
width="140"
>
<!-- <template slot-scope="scope">
</template> -->
</el-table-column>
</el-table> </el-table>
</div> </el-dialog>
</base-model>
</template> </template>
<script> <script>
import baseModel from "@/components/BaseModel"; import baseModel from "@/components/BaseModel";
import { getSubjectImageUploadList } from "@/api/load.js"; import {
getSubjectImageUploadList,
preArchiveDicomStudy,
addOrUpdateArchiveTaskStudy,
deleteTaskStudy,
} from "@/api/load.js";
import { downloadImage } from "@/utils/uploadZip.js";
import { parseDicom, getThumbnail, dicomToOSS } from "@/utils/parseDicom.js";
export default { export default {
name: "uploadImage", name: "uploadImage",
props: { props: {
@ -122,35 +328,31 @@ export default {
}, },
data() { data() {
return { return {
model_cfg: { isLoad: false,
visible: false,
showClose: true,
width: "1000px",
title: this.$t("trials:uploadImage:title:uploadImages"),
appendToBody: true,
},
list: [], list: [],
StudyInstanceUidList: [],
SopInstanceUidList: [],
uploadList: [], //
dicomList: [], //
requestNum: 6,
}; };
}, },
watch: {
visible: {
handler() {
this.model_cfg.visible = this.visible;
},
immediate: true,
deep: true,
},
"model_cfg.visible": {
handler() {
this.$emit("update:visible", this.model_cfg.visible);
},
deep: true,
},
},
created() { created() {
this.getList(); this.getList();
}, },
methods: { methods: {
beforeClose() {
this.$emit("update:visible", false);
},
notUp() {
this.$confirm(this.$t("trials:uploadImage:confirmMessage:notUp"), {
type: "warning",
distinguishCancelAndClose: true,
confirmButtonText: this.$t("common:button:confirm"),
cancelButtonText: this.$t("common:button:cancel"),
});
},
// //
async getList() { async getList() {
try { try {
@ -161,7 +363,25 @@ export default {
let res = await getSubjectImageUploadList(params); let res = await getSubjectImageUploadList(params);
this.loading = false; this.loading = false;
if (res.IsSuccess) { if (res.IsSuccess) {
this.StudyInstanceUidList = [];
this.SopInstanceUidList = [];
this.list = res.Result; this.list = res.Result;
res.Result.forEach((item) => {
if (item.OrginalStudyList && Array.isArray(item.OrginalStudyList)) {
item.OrginalStudyList.forEach((data) => {
data.SubjectId = item.SubejctId;
data.VisitTaskId = item.VisitTaskId;
data.SourceSubjectVisitId = item.SourceSubjectVisitId;
this.StudyInstanceUidList.push(data);
});
}
if (item.UploadStudyList && Array.isArray(item.UploadStudyList)) {
item.UploadStudyList.forEach((data) => {
data.SopInstanceUidList &&
this.SopInstanceUidList.push(...data.SopInstanceUidList);
});
}
});
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@ -169,7 +389,470 @@ export default {
} }
}, },
// //
remove(item) {}, async remove(item) {
try {
let confirm = await this.$confirm(
this.$t("trials:uploadImage:confirm:delMessage"),
{
type: "warning",
distinguishCancelAndClose: true,
confirmButtonText: this.$t("common:button:confirm"),
cancelButtonText: this.$t("common:button:cancel"),
}
);
if (confirm !== "confirm") return;
let params = {
VisitTaskId: item.VisitTaskId,
};
let res = await deleteTaskStudy(params);
if (res.IsSuccess) {
this.getList();
this.$message.success(
this.$t("trials:uploadImage:message:delSuccess")
);
}
} catch (err) {
console.log(err);
}
},
//
async downloadImage(item) {
try {
await downloadImage(
this.$route.query.trialId,
item.SourceSubjectVisitId
);
} catch (err) {
console.log(err);
}
},
//
async preArchiveDicomStudy(post, index) {
try {
let res = await preArchiveDicomStudy(post);
if (res.IsSuccess) {
let item = this.uploadList[index];
item.StudyMonitorId = res.Result;
let arr = this.dicomList.filter(
(dicom) => dicom.StudyInstanceUid === item.StudyInstanceUid
);
let num = arr.length > this.requestNum ? this.requestNum : arr.length;
let funArr = [];
for (let i = 0; i < num; i++) {
funArr.push(this.handleUploadTask(arr, i));
}
if (funArr.length > 0) {
let res = await Promise.all(funArr);
}
} else {
return false;
}
} catch (err) {
console.log(err);
return false;
}
},
//
async beginScanFiles(e) {
let files = e.target.files;
try {
this.dicomList = [];
for (var i = 0; i < files.length; ++i) {
let dicom = await parseDicom(files[i]);
let has = this.StudyInstanceUidList.some(
(item) => item.StudyInstanceUid === dicom.StudyInstanceUid
);
if (
!dicom ||
!has ||
this.SopInstanceUidList.includes(dicom.SopInstanceUid)
) {
if (i === files.length - 1 && this.dicomList.length <= 0) {
let confirm = await this.$confirm(
this.$t("trials:uploadImage:confirmMessage:failSubject"),
{
type: "warning",
distinguishCancelAndClose: true,
confirmButtonText: this.$t("common:button:confirm"),
cancelButtonText: this.$t("common:button:cancel"),
}
);
if (confirm !== "confirm") continue;
this.$refs.file.click();
}
continue;
}
this.isLoad = true;
dicom.file = files[i];
dicom.isUpload = 0;
this.StudyInstanceUidList.some((item) => {
if (item.StudyInstanceUid === dicom.StudyInstanceUid) {
dicom.params = {
TrialId: this.$route.query.trialId,
SubjectId: item.SubjectId,
VisitTaskId: item.VisitTaskId,
SourceSubjectVisitId: item.SourceSubjectVisitId,
};
let uploadHas = this.uploadList.some((uploadData) => {
if (uploadData.StudyInstanceUid === dicom.StudyInstanceUid) {
if (uploadData.status > 0) {
uploadData.count = 1;
uploadData.successCount = 0;
uploadData.failCount = 0;
uploadData.schedule = 0;
uploadData.status = 0;
uploadData.FileSize = dicom.file.size;
uploadData.IsDicomReUpload = false;
uploadData.SeriesInstanceUidList = [
dicom.SeriesInstanceUid,
];
uploadData.dicomInfo = {
AccessionNumber: dicom.AccessionNumber,
Modalities: dicom.Modalities,
seriesBodyPartExamined: dicom.seriesBodyPartExamined,
StudyDescription: dicom.StudyDescription,
StudyTime: this.getTime(dicom.StudyDate, dicom.StudyTime),
PatientName: dicom.PatientName,
PatientId: dicom.PatientId,
PatientAge: dicom.PatientAge,
PatientSex: dicom.PatientSex,
PatientBirthDate: dicom.PatientBirthDate,
};
} else {
uploadData.FileSize += dicom.file.size;
uploadData.count++;
}
if (
!uploadData.SeriesInstanceUidList.includes(
dicom.SeriesInstanceUid
)
) {
uploadData.SeriesInstanceUidList.push(
dicom.SeriesInstanceUid
);
}
if (
item.SopInstanceUidList &&
item.SopInstanceUidList.length > 0
) {
uploadData.IsDicomReUpload = true;
}
}
return uploadData.StudyInstanceUid === dicom.StudyInstanceUid;
});
if (!uploadHas) {
let uploadData = {
TrialId: this.$route.query.trialId,
SubjectId: item.SubjectId,
SourceSubjectVisitId: item.SourceSubjectVisitId,
StudyId: dicom.StudyId,
StudyInstanceUid: dicom.StudyInstanceUid,
FileSize: dicom.file.size,
count: 1,
successCount: 0,
failCount: 0,
schedule: 0,
status: 0,
SeriesInstanceUidList: [dicom.SeriesInstanceUid],
IsDicomReUpload: false,
dicomInfo: {
AccessionNumber: dicom.AccessionNumber,
Modalities: dicom.Modalities,
seriesBodyPartExamined: dicom.seriesBodyPartExamined,
StudyDescription: dicom.StudyDescription,
StudyTime: this.getTime(dicom.StudyDate, dicom.StudyTime),
PatientName: dicom.PatientName,
PatientId: dicom.PatientId,
PatientAge: dicom.PatientAge,
PatientSex: dicom.PatientSex,
PatientBirthDate: dicom.PatientBirthDate,
},
};
if (
item.SopInstanceUidList &&
item.SopInstanceUidList.length > 0
) {
uploadData.IsDicomReUpload = true;
}
this.uploadList.push(uploadData);
}
}
return item.StudyInstanceUid === dicom.StudyInstanceUid;
});
this.dicomList.push(dicom);
}
let funArr = [];
for (let i = 0; i < this.uploadList.length; i++) {
let item = this.uploadList[i];
let data = {
TrialId: item.TrialId,
SubjectId: item.SubjectId,
SubjectVisitId: item.SourceSubjectVisitId,
FileSize: item.FileSize,
IsDicomReUpload: item.IsDicomReUpload,
FileCount: item.count,
};
funArr.push(this.preArchiveDicomStudy(data, i));
}
if (funArr && funArr.length > 0) {
let res = await Promise.all(funArr);
}
} catch (err) {
console.log(err);
}
},
//
async handleUploadTask(arr, index) {
arr[index].isUpload = 1;
let path = `/${arr[index].params.TrialId}/TaskImage/${
arr[index].params.SubjectId
}/${arr[index].params.VisitTaskId}/${
arr[index].StudyInstanceUid
}/${this.getGuid(
arr[index].StudyInstanceUid +
arr[index].SeriesInstanceUid +
arr[index].SOPInstanceUid +
arr[index].params.TrialId +
arr[index].params.VisitTaskId
)}`;
let uploadDicom = this.uploadList.filter(
(item) => item.StudyInstanceUid === arr[index].StudyInstanceUid
)[0];
let res = await dicomToOSS(arr[index].file, path);
if (res) {
arr[index].path = res;
uploadDicom.successCount++;
arr[index].isUpload = 2;
} else {
uploadDicom.status = 2;
uploadDicom.failCount++;
arr[index].isUpload = 3;
}
uploadDicom.schedule = Math.floor(
((uploadDicom.successCount + uploadDicom.failCount) /
uploadDicom.count) *
100
);
if (
uploadDicom.schedule >= 100 &&
uploadDicom.successCount === uploadDicom.count
) {
uploadDicom.status = 1;
let res = await this.formatDicom(arr, arr[index].StudyInstanceUid);
if (res) {
uploadDicom.Study = res[arr[index].StudyId];
uploadDicom.status = 3;
try {
res = await addOrUpdateArchiveTaskStudy(uploadDicom);
if (res.IsSuccess) {
uploadDicom.status = 4;
this.getList();
} else {
uploadDicom.status = 5;
}
let flag = this.uploadList.every((item) => item.status > 3);
if (flag) this.isLoad = false;
} catch (err) {
uploadDicom.status = 5;
let flag = this.uploadList.every((item) => item.status > 3);
if (flag) this.isLoad = false;
}
}
}
let ind = arr.findIndex((item) => !item.isUpload);
if (ind >= 0) {
return this.handleUploadTask(arr, ind);
} else {
return false;
}
},
//
async formatDicom(arr, StudyInstanceUid) {
let studyObj = {};
for (let i = 0; i < arr.length; i++) {
let dicom = arr[i];
if (dicom.StudyInstanceUid !== StudyInstanceUid) continue;
let thumbnailPath = `/${dicom.params.TrialId}/TaskImage/${dicom.params.SubjectId}/${dicom.params.VisitTaskId}/${dicom.StudyInstanceUid}/${dicom.SeriesInstanceUid}.png`;
if (!studyObj[dicom.StudyId]) {
studyObj[dicom.StudyId] = {
StudyId: dicom.StudyId,
StudyInstanceUid: dicom.StudyInstanceUid,
StudyTime: this.getTime(dicom.StudyDate, dicom.StudyTime),
Modalities: dicom.Modalities,
Description: dicom.StudyDescription,
SeriesCount: 1,
InstanceCount: 1,
InstitutionName: dicom.InstitutionName,
PatientId: dicom.PatientId,
PatientName: dicom.PatientName,
PatientAge: dicom.PatientAge,
PatientSex: dicom.PatientSex,
AccessionNumber: dicom.AccessionNumber,
PatientBirthDate: dicom.PatientBirthDate,
AcquisitionTime: dicom.AcquisitionTime,
AcquisitionNumber: dicom.AcquisitionNumber,
TriggerTime: dicom.TriggerTime,
BodyPartExamined: dicom.BodyPartExamined,
SeriesList: [
{
StudyInstanceUid: dicom.StudyInstanceUid,
SeriesInstanceUid: dicom.SeriesInstanceUid,
SeriesNumber: dicom.SeriesNumber,
SeriesTime: this.getTime(dicom.SeriesDate, dicom.SeriesTime),
Modality: dicom.Modality,
Description: dicom.SeriesDescription,
InstanceCount: 1,
SliceThickness: dicom.SliceThickness,
ImagePositionPatient: dicom.ImagePosition,
ImageOrientationPatient: dicom.ImageOrientation,
BodyPartExamined: dicom.seriesBodyPartExamined,
SequenceName: dicom.SequenceName,
ProtocolName: dicom.ProtocolName,
ImagerPixelSpacing: dicom.ImagePixelSpacing,
AcquisitionTime: dicom.AcquisitionTime,
AcquisitionNumber: dicom.AcquisitionNumber,
TriggerTime: dicom.TriggerTime,
ImageResizePath: await getThumbnail(
dicom.file,
thumbnailPath,
dicom
),
InstanceList: [
{
StudyInstanceUid: dicom.StudyInstanceUid,
SeriesInstanceUid: dicom.SeriesInstanceUid,
SopInstanceUid: dicom.SopInstanceUid,
InstanceNumber: dicom.ImageOrInstanceNumber,
InstanceTime: this.getTime(
dicom.ContentDate,
dicom.ContentTime
),
ImageRows: dicom.Rows || 0,
ImageColumns: dicom.Columns || 0,
SliceLocation: dicom.SliceLocation,
SliceThickness: dicom.SliceThickness,
NumberOfFrames: dicom.NumberOfFrames || 0,
PixelSpacing: dicom.PixelSpacing,
ImagerPixelSpacing: dicom.ImagePixelSpacing,
FrameOfReferenceUID: dicom.FrameOfReferenceUID,
WindowCenter: dicom.WindowCenter,
WindowWidth: dicom.WindowWidth,
Path: dicom.path,
HtmlPath: "",
},
],
},
],
};
} else {
let seriesHas = studyObj[dicom.StudyId].SeriesList.some((item) => {
if (item.SeriesInstanceUid === dicom.SeriesInstanceUid) {
item.InstanceCount++;
let instance = {
StudyInstanceUid: dicom.StudyInstanceUid,
SeriesInstanceUid: dicom.SeriesInstanceUid,
SopInstanceUid: dicom.SopInstanceUid,
InstanceNumber: dicom.ImageOrInstanceNumber,
InstanceTime: this.getTime(
dicom.ContentDate,
dicom.ContentTime
),
ImageRows: dicom.Rows || 0,
ImageColumns: dicom.Columns || 0,
SliceLocation: dicom.SliceLocation,
SliceThickness: dicom.SliceThickness,
NumberOfFrames: dicom.NumberOfFrames || 0,
PixelSpacing: dicom.PixelSpacing,
ImagerPixelSpacing: dicom.ImagePixelSpacing,
FrameOfReferenceUID: dicom.FrameOfReferenceUID,
WindowCenter: dicom.WindowCenter,
WindowWidth: dicom.WindowWidth,
Path: dicom.path,
HtmlPath: "",
};
item.InstanceList.push(instance);
}
return item.SeriesInstanceUid === dicom.SeriesInstanceUid;
});
if (!seriesHas) {
studyObj[dicom.StudyId].SeriesCount++;
let series = {
StudyInstanceUid: dicom.StudyInstanceUid,
SeriesInstanceUid: dicom.SeriesInstanceUid,
SeriesNumber: dicom.SeriesNumber,
SeriesTime: this.getTime(dicom.SeriesDate, dicom.SeriesTime),
Modality: dicom.Modality,
Description: dicom.SeriesDescription,
InstanceCount: 1,
SliceThickness: dicom.SliceThickness,
ImagePositionPatient: dicom.ImagePosition,
ImageOrientationPatient: dicom.ImageOrientation,
BodyPartExamined: dicom.seriesBodyPartExamined,
SequenceName: dicom.SequenceName,
ProtocolName: dicom.ProtocolName,
ImagerPixelSpacing: dicom.ImagePixelSpacing,
AcquisitionTime: dicom.AcquisitionTime,
AcquisitionNumber: dicom.AcquisitionNumber,
TriggerTime: dicom.TriggerTime,
ImageResizePath: await getThumbnail(
dicom.file,
thumbnailPath,
dicom
),
InstanceList: [
{
StudyInstanceUid: dicom.StudyInstanceUid,
SeriesInstanceUid: dicom.SeriesInstanceUid,
SopInstanceUid: dicom.SopInstanceUid,
InstanceNumber: dicom.ImageOrInstanceNumber,
InstanceTime: this.getTime(
dicom.ContentDate,
dicom.ContentTime
),
ImageRows: dicom.Rows || 0,
ImageColumns: dicom.Columns || 0,
SliceLocation: dicom.SliceLocation,
SliceThickness: dicom.SliceThickness,
NumberOfFrames: dicom.NumberOfFrames || 0,
PixelSpacing: dicom.PixelSpacing,
ImagerPixelSpacing: dicom.ImagePixelSpacing,
FrameOfReferenceUID: dicom.FrameOfReferenceUID,
WindowCenter: dicom.WindowCenter,
WindowWidth: dicom.WindowWidth,
Path: dicom.path,
HtmlPath: "",
},
],
};
studyObj[dicom.StudyId].SeriesList.push(series);
}
studyObj[dicom.StudyId].InstanceCount++;
}
}
console.log(studyObj);
return studyObj;
},
// dicom
getTime(date, time) {
let studyTime = "";
if (date) {
date = `${date.substring(0, 4)}-${date.substring(
4,
6
)}-${date.substring(6, 8)}`;
}
if (time) {
time = `${time.substring(0, 2)}:${time.substring(
2,
4
)}:${time.substring(4, 6)}`;
}
if (date) {
studyTime = time ? `${date} ${time}` : `${date} 00:00:00`;
}
return studyTime;
},
}, },
}; };
</script> </script>
@ -177,4 +860,12 @@ export default {
label { label {
cursor: pointer; cursor: pointer;
} }
::v-deep .el-dialog {
.el-dialog__header {
padding-top: 15px;
}
}
.colorOfRed {
color: #f44336;
}
</style> </style>

View File

@ -1,12 +1,13 @@
import Vue from 'vue' import Vue from 'vue'
import { encoder } from './encoder'
import { anonymization } from './anonymization' import { anonymization } from './anonymization'
export const dcmUpload = async function (name, file, config) { export const dcmUpload = async function (name, file, config) {
return new Promise(async resolve => { return new Promise(async resolve => {
try { try {
// let blob = await encoder(file, config) // let blob = await encoder(file, config)
// let blob = await fileToBlob(file) let blob = await fileToBlob(file)
let blob = await anonymization(file, config) if (config) {
blob = await anonymization(file, config)
}
let res = await Vue.prototype.OSSclient.put(name, blob.blob) let res = await Vue.prototype.OSSclient.put(name, blob.blob)
resolve({ resolve({
...res, ...res,
@ -35,7 +36,7 @@ function fileToBlob(file) {
} else { } else {
blob = e.target.result blob = e.target.result
} }
resolve(blob) resolve({ blob })
}) })
// FileReader 以 ArrayBuffer 格式 读取 File 对象中数据 // FileReader 以 ArrayBuffer 格式 读取 File 对象中数据
reader.readAsArrayBuffer(file) reader.readAsArrayBuffer(file)

200
src/utils/parseDicom.js Normal file
View File

@ -0,0 +1,200 @@
import * as dicomParser from "dicom-parser";
import * as cornerstone from "cornerstone-core";
import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
import { convertBytes } from "@/utils/dicom-character-set";
import Vue from 'vue';
import { dcmUpload } from "@/utils/dcmUpload/dcmUpload";
let dicom = {
PatientName: "x00100010", // 患者姓名
PatientId: "x00100020", // 患者ID
PatientBirthDate: "x00100030", // 患者出生日期
PatientBirthTime: "x00100032", // 患者出生时间
PatientSex: "x00100040", // 患者性别
PatientWeight: "x00101030", // 患者体重
PregnancyStatus: "x001021c0", // 怀孕状态
PatientAge: "x00101010", // 患者年龄(做检查时刻的患者年龄,而不是此刻患者的真实年龄)
AcquisitionTime: "x00080032",
AcquisitionNumber: "x00200012",
TriggerTime: "x00181060",
AccessionNumber: "x00080050", // 检查号
StudyId: "x00200010", // 检查ID
StudyInstanceUid: "x0020000d", // 检查实例号
StudyDate: "x00080020", // 检查日期
StudyTime: "x00080030", // 检查时间
Modalities: "x00080061", // 一个检查中含有的不同检查类型
BodyPartExamined: "x00080015", // 检查的部位
StudyDescription: "x00081030", // 检查的描述
InstitutionName: "x00080080",
SeriesNumber: "x00200011", // 序列号
SeriesInstanceUid: "x0020000e", // 序列实例号
Modality: "x00080060", // 检查模态
SeriesDescription: "x0008103e", // 检查描述和说明
SeriesDate: "x00080021", // 检查日期
SeriesTime: "x00080031", // 检查时间
SequenceName: "x00180024",
ProtocolName: "x00181030",
ImagePosition: "x00200032", // 图像位置
ImageOrientation: "x00200037", // 图像方位
ImagePixelSpacing: "x00181164",
SliceThickness: "x00180050", // 层厚
SpacingBetweenSlices: "x00180088", // 层间距
SliceLocation: "x00201041", // 相对位置
MRAcquisition: "x00180023",
seriesBodyPartExamined: "x00180015", // 身体部位
ImageType: "x00080008",
SopInstanceUid: "x00080018", // SOP实例UID
ContentDate: "x00080023", // 影像拍摄日期
ContentTime: "x00080033", // 影像拍摄时间
ImageOrInstanceNumber: "x00200013", // 图像码
SamplesPerPixel: "x00280002", // 图像采样率
PhotometricInterpretation: "x00280004", // 光度计对于CT图像用两个枚举值MONOCHROME1MONOCHROME2 用来判断图像是否是彩色的MONOCHROME 1/2是灰度图RGB则是真彩色图
Rows: "x00280010", // 行数
Columns: "x00280011", // 列数
NumberOfFrames: "x00280008",
PixelSpacing: "x00280030", // 像素间距
BitsAllocated: "x00280100", // 分配的位数
BitsStored: "x00280101", // 存储的位数
HighBit: "x00280102", // 高位
PixelRepresentation: "x00280103", // 像素数据的表现类型一个枚举值分别为十六进制数0000和0001.0000H = 无符号整型0001H = 2的补码
WindowCenter: "x00281050", // 窗位
WindowWidth: "x002811051", // 窗宽
RescaleIntercept: "x00281052", // 截距
RescaleSlope: "x00281053", // 斜率
RescaleType: "x00281054", // 输出值的单位
ParsingFormat: "x00080005",
FrameOfReferenceUID: "x00200052",
};
// 需要设置默认值
let defaultKey = ['Rows', 'Columns', 'SliceLocation', 'NumberOfFrames'];
// 需要格式解析
let pormatParseKey = ['PatientName', 'SeriesDescription', 'StudyDescription'];
// 解析dicom文件
export const parseDicom = (file, name = false) => {
return new Promise(function (resolve, reject) {
let reader = new FileReader();
reader.onload = function (e) {
try {
let data = dicomParser.parseDicom(new Uint8Array(e.target.result));
let res = {};
if (name && Array.isArray(name)) {
name.forEach((item) => {
if (dicom[item]) {
res[item] = data.string(dicom[item]) || data.uint16(dicom[item]) || data.intString(dicom[item]) || '';
}
if (item === 'SliceLocation') {
res[item] = data.intString(dicom[item])
}
});
} else if (name) {
if (dicom[name]) {
res[name] = data.string(dicom[name]) || data.uint16(dicom[name]) || data.intString(dicom[name]) || '';
if (name === 'SliceLocation') {
res[name] = data.intString(dicom[name])
}
} else {
reject("name is inexistence");
}
} else {
Object.keys(dicom).forEach((key) => {
res[key] = data.string(dicom[key]) || data.uint16(dicom[key]) || data.intString(dicom[key]) || '';
if (key === 'SliceLocation') {
res[key] = data.intString(dicom[key])
}
});
}
pormatParseKey.forEach(key => {
if (res[key]) {
const Element = data.elements[dicom[key]];
const Bytes = new Uint8Array(
data.byteArray.buffer,
Element ? Element.dataOffset : 0,
Element ? Element.length : 0
);
res[key] = convertBytes(
res.ParsingFormat,
Bytes
);
}
})
defaultKey.forEach(key => {
if (!res[key] && res.hasOwnProperty(key)) {
res[key] = 0;
}
})
resolve(res);
} catch (error) {
reject(error);
}
};
reader.onerror = function (e) {
reject(e);
};
reader.readAsArrayBuffer(file);
});
};
// 影像上传
// 影像上传
export const dicomToOSS = async (file, path) => {
try {
let res = await dcmUpload(path, file);
if (!res || !res.url) return false;
return Vue.prototype.$getObjectName(res.url);
} catch (err) {
console.log(err);
return false;
}
};
// 获取缩略图
export const getThumbnail = async (file, ossPath, dicomInfo) => {
try {
if (dicomInfo.modality !== "SR") {
let fileId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
let blob = await dicomToPng(
fileId,
dicomInfo.Columns,
dicomInfo.Rows
);
if (!blob) return "";
let OSSclient = Vue.prototype.OSSclient;
let seriesRes = await OSSclient.put(ossPath, blob);
if (seriesRes && seriesRes.url) {
return Vue.prototype.$getObjectName(seriesRes.url);
} else {
return "";
}
} else {
return "";
}
} catch (err) {
console.log(err);
return "";
}
};
const canvasToBlob = (canvas) => {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
});
});
};
const dicomToPng = (imageId, width, height) => {
return new Promise((resolve) => {
cornerstone.loadImage(imageId).then(async (image) => {
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
if (image) {
cornerstone.renderToCanvas(canvas, image);
// 将 Canvas 图像对象转换为 PNG 格式
let blob = await canvasToBlob(canvas);
resolve(blob);
} else {
resolve(false);
}
});
}).catch((reason) => {
reason();
});
};

View File

@ -43,6 +43,7 @@
prop="OrganizationName" prop="OrganizationName"
> >
<el-input <el-input
:disabled="user.IsZhiZhun"
v-model="user.OrganizationName" v-model="user.OrganizationName"
:placeholder="$t('trials:trials-myinfo:form:organization')" :placeholder="$t('trials:trials-myinfo:form:organization')"
/> />

View File

@ -617,7 +617,7 @@ export default {
}); });
}, },
applySync(ReadingQuestionTrialId, index) { applySync(ReadingQuestionTrialId, index) {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
console.log(this.QuestionList[index].JudgeType); console.log(this.QuestionList[index].JudgeType);
if (this.QuestionList[index].JudgeType === 0) { if (this.QuestionList[index].JudgeType === 0) {
reject(false); reject(false);
@ -637,6 +637,21 @@ export default {
reject(false); reject(false);
return; return;
} }
if (
this.QuestionList[index].JudgeType === 4 ||
this.QuestionList[index].JudgeType === 5
) {
try {
let validate = await this.$refs[
"JudgeDifferenceValue" +
this.QuestionList[index].JudgeType +
index
][0].validate();
if (!validate) return reject(false);
} catch (err) {
return reject(false);
}
}
this.btnLoading = true; this.btnLoading = true;
this.loading = true; this.loading = true;
setTrialCriterionJudgeQuestionAnswerGroup({ setTrialCriterionJudgeQuestionAnswerGroup({
@ -644,6 +659,12 @@ export default {
AnswerGroup: this.QuestionList[index].AnswerGroup2List, AnswerGroup: this.QuestionList[index].AnswerGroup2List,
AnswerCombination: this.QuestionList[index].AnswerGroupList, AnswerCombination: this.QuestionList[index].AnswerGroupList,
JudgeType: this.QuestionList[index].JudgeType, JudgeType: this.QuestionList[index].JudgeType,
JudgeDifferenceValue: Number(
this.QuestionList[index].JudgeDifferenceValue
),
JudgeDifferenceType: Number(
this.QuestionList[index].JudgeDifferenceType
),
}) })
.then((res) => { .then((res) => {
resolve(); resolve();
@ -778,6 +799,8 @@ export default {
this.$set(this.QuestionList[index], "AnswerGroup2List", []); this.$set(this.QuestionList[index], "AnswerGroup2List", []);
this.$set(this.QuestionList[index], "AnswerGroupList", []); this.$set(this.QuestionList[index], "AnswerGroupList", []);
this.$set(this.QuestionList[index], "JudgeType", 0); this.$set(this.QuestionList[index], "JudgeType", 0);
this.$set(this.QuestionList[index], "JudgeDifferenceValue", 0);
this.$set(this.QuestionList[index], "JudgeDifferenceType", 0);
this.$message.success(this.$t("trials:adRules:message:msg7")); // '' this.$message.success(this.$t("trials:adRules:message:msg7")); // ''
this.btnLoading = false; this.btnLoading = false;
this.loading = false; this.loading = false;

View File

@ -17,7 +17,7 @@
> >
<template v-for="item of $d.Criterion_Question_Type"> <template v-for="item of $d.Criterion_Question_Type">
<el-option <el-option
v-if="item.value !== 'calculation' && item.value !== 'increment'" v-if="item.value !== 'calculation' && item.value !== 'increment'&& item.value !== 'table'&& item.value !== 'group'"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
:label="item.label" :label="item.label"

View File

@ -285,7 +285,7 @@ export default {
handleSubjectVisitChange(val) { handleSubjectVisitChange(val) {
if (val) { if (val) {
const selectArr = this.subjectVisitOptions.filter(item => item.Id === val) const selectArr = this.subjectVisitOptions.filter(item => item.Id === val)
this.form.VisitNum = selectArr[0].VisitNum + 0.1 this.form.VisitNum = Math.ceil((selectArr[0].VisitNum + 0.1)*10)/10;
} }
}, },
handleIsFinalVisitChange(val) { handleIsFinalVisitChange(val) {

View File

@ -977,7 +977,15 @@ const PDFViewerApplication = {
await Promise.all(promises); await Promise.all(promises);
}, },
decodeUtf8(bytes) {
let str = bytes.split('?');
let str2 = str[0].split('/');
let name = str2[str2.length-1];
name = encodeURIComponent(name);
str.shift();
str2.pop();
return str2.join("/")+'/'+name+'?'+str.join("?");
},
async open(file, args) { async open(file, args) {
if (this.pdfLoadingTask) { if (this.pdfLoadingTask) {
await this.close(); await this.close();
@ -992,6 +1000,7 @@ const PDFViewerApplication = {
const parameters = Object.create(null); const parameters = Object.create(null);
if (typeof file === "string") { if (typeof file === "string") {
file = this.decodeUtf8(file);
this.setTitleUsingUrl(file, file); this.setTitleUsingUrl(file, file);
parameters.url = file; parameters.url = file;
} else if (file && "byteLength" in file) { } else if (file && "byteLength" in file) {
@ -2228,12 +2237,12 @@ function webViewerInitialized() {
const appConfig = PDFViewerApplication.appConfig; const appConfig = PDFViewerApplication.appConfig;
let file; let file;
const queryString = decodeURIComponent(document.location.search.substring(1)); const queryString = decodeURIComponent(document.location.search.substring(1));
console.log(queryString) // console.log(queryString)
let qs = queryString.replaceAll('+', '%2B') // let qs = queryString.replaceAll('+', '%2B')
// queryString = queryString.replaceAll('+', '%20') // queryString = queryString.replaceAll('+', '%20')
// const params = (0, _ui_utils.parseQueryString)(queryString); // const params = (0, _ui_utils.parseQueryString)(queryString);
// file = params.get("file") ?? _app_options.AppOptions.get("defaultUrl"); // file = params.get("file") ?? _app_options.AppOptions.get("defaultUrl");
file = qs.replace('file=', '') file = queryString.replace('file=', '')
validateFileURL(file); validateFileURL(file);
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.id = appConfig.openFileInputName; fileInput.id = appConfig.openFileInputName;