feat: 下载dicom
This commit is contained in:
parent
d63eb1a5af
commit
c6834cb4c4
|
@ -1,4 +1,4 @@
|
||||||
import { Apis, ExistInPacsDTO } from "@@/infra/api";
|
import { Apis, DownloadArchiveDTO, ExistInPacsDTO } from "@@/infra/api";
|
||||||
|
|
||||||
export class DicomRepository {
|
export class DicomRepository {
|
||||||
async upload2Pacs(dcmFile: File) {
|
async upload2Pacs(dcmFile: File) {
|
||||||
|
@ -10,4 +10,8 @@ export class DicomRepository {
|
||||||
async existInPacs(p: ExistInPacsDTO) {
|
async existInPacs(p: ExistInPacsDTO) {
|
||||||
return Apis.existInPacs(p);
|
return Apis.existInPacs(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadDicom(p: DownloadArchiveDTO) {
|
||||||
|
return Apis.downloadDicom(p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ExistInPacsDTO } from "@@/infra/api";
|
import { DownloadArchiveDTO, ExistInPacsDTO } from "@@/infra/api";
|
||||||
import { DicomRepository } from "./DicomRepository";
|
import { DicomRepository } from "./DicomRepository";
|
||||||
|
|
||||||
export class DicomService {
|
export class DicomService {
|
||||||
|
@ -17,4 +17,8 @@ export class DicomService {
|
||||||
async existInPacs(p: ExistInPacsDTO) {
|
async existInPacs(p: ExistInPacsDTO) {
|
||||||
return await this.dicomRepository.existInPacs(p);
|
return await this.dicomRepository.existInPacs(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadDicom(p: DownloadArchiveDTO) {
|
||||||
|
return await this.dicomRepository.downloadDicom(p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { User } from "@@/domain/User/entities/User";
|
import { User } from "@@/domain/User/entities/User";
|
||||||
import { Request } from "./Request";
|
import { Request } from "./Request";
|
||||||
import { Study } from "@/modules/Admin/Dicom/Upload/DicomUploader/util";
|
import { Study } from "@/modules/Admin/Dicom/Upload/DicomUploader/util";
|
||||||
|
import axios from "axios";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
const PREFIX = "/api/dmp";
|
const PREFIX = "/api/dmp";
|
||||||
const PREFIX_CERT = "/cert";
|
const PREFIX_CERT = "/cert";
|
||||||
|
@ -31,6 +33,11 @@ export type ArchiveTaskCreateDto = {
|
||||||
study: Study[];
|
study: Study[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DownloadArchiveDTO = {
|
||||||
|
ID: string;
|
||||||
|
Type: "study" | "series" | "Study" | "Series";
|
||||||
|
};
|
||||||
|
|
||||||
export const Apis = {
|
export const Apis = {
|
||||||
/**
|
/**
|
||||||
* 用户登录
|
* 用户登录
|
||||||
|
@ -80,4 +87,20 @@ export const Apis = {
|
||||||
Request.get(PREFIX + "/annotator/find/archiveTask"),
|
Request.get(PREFIX + "/annotator/find/archiveTask"),
|
||||||
|
|
||||||
findDicoms: (): ResponseType => Request.get(PREFIX + "/admin/find/dicom/all"),
|
findDicoms: (): ResponseType => Request.get(PREFIX + "/admin/find/dicom/all"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载dicom zip
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
downloadDicom: (p: DownloadArchiveDTO) =>
|
||||||
|
axios
|
||||||
|
.post(PREFIX + `/dicom/download`, p, { responseType: "blob" })
|
||||||
|
.then((response) => {
|
||||||
|
const filename = response.headers["content-name"];
|
||||||
|
const blob = new Blob([response.data], { type: "application/zip" });
|
||||||
|
saveAs(blob, filename);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Download error:", error);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
"@ant-design/icons": "5.2.5",
|
"@ant-design/icons": "5.2.5",
|
||||||
"dicom-parser": "1.8.21",
|
"dicom-parser": "1.8.21",
|
||||||
"path-to-regexp": "6.2.1",
|
"path-to-regexp": "6.2.1",
|
||||||
"@tavi/util": "workspace:*"
|
"@tavi/util": "workspace:*",
|
||||||
|
"file-saver": "2.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.1",
|
"eslint-plugin-react-refresh": "^0.4.1",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.0"
|
"vite": "^4.4.0",
|
||||||
|
"@types/file-saver": "2.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,35 +1,74 @@
|
||||||
import { TableColumnsType } from "antd";
|
import { TableColumnsType } from "antd";
|
||||||
import { DataItemType } from ".";
|
import { SeriesItemType, StudyItemType } from ".";
|
||||||
|
|
||||||
export const columnsForStudy: TableColumnsType<DataItemType> = [
|
export const columnsForStudy: TableColumnsType<StudyItemType> = [
|
||||||
{ title: "PatientID", dataIndex: "PatientID", key: "PatientID" },
|
{ title: "PatientID", dataIndex: "PatientID", key: "PatientID" },
|
||||||
{ title: "患者姓名", dataIndex: "PatientName", key: "PatientName" },
|
{ title: "患者姓名", dataIndex: "PatientName", key: "PatientName" },
|
||||||
{ title: "性别", dataIndex: "PatientSex", key: "PatientSex" },
|
{ title: "性别", dataIndex: "PatientSex", key: "PatientSex" },
|
||||||
{ title: "出生日期", dataIndex: "PatientBirthDate", key: "PatientBirthDate" },
|
{ title: "出生日期", dataIndex: "PatientBirthDate", key: "PatientBirthDate" },
|
||||||
{ title: "病例日期", dataIndex: "StudyDate", key: "StudyDate" },
|
{ title: "病例日期", dataIndex: "StudyDate", key: "StudyDate" },
|
||||||
{
|
{ title: "机构", dataIndex: "InstitutionName", key: "InstitutionName" },
|
||||||
title: "相关序列数",
|
|
||||||
dataIndex: "NumberPatientRelatedInstances",
|
|
||||||
key: "NumberPatientRelatedInstances",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const columnsForSeries: TableColumnsType<DataItemType> = [
|
export const columnsForSeries: TableColumnsType<SeriesItemType> = [
|
||||||
{ title: "序列号", dataIndex: "SeriesNumber", key: "SeriesNumber" },
|
|
||||||
{
|
|
||||||
title: "成像设备",
|
|
||||||
dataIndex: "Modality",
|
|
||||||
key: "Modality",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "序列描述",
|
title: "序列描述",
|
||||||
dataIndex: "SeriesDescription",
|
dataIndex: "SeriesDescription",
|
||||||
key: "SeriesDescription",
|
key: "SeriesDescription",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<div>
|
||||||
|
<span>采集体位</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: "PatientPosition",
|
||||||
|
render: (_, record: SeriesItemType) => {
|
||||||
|
return <>{record.tags.PatientPosition}</>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "检查区域",
|
||||||
|
key: "BodyPartExamined",
|
||||||
|
render: (_, record: SeriesItemType) => {
|
||||||
|
return <>{record.tags.BodyPartExamined}</>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "切片数",
|
title: "切片数量",
|
||||||
dataIndex: "NumberSeriesRelatedInstances",
|
dataIndex: "InstanceNumber",
|
||||||
key: "NumberSeriesRelatedInstances",
|
key: "InstanceNumber",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "层厚",
|
||||||
|
key: "SliceThickness",
|
||||||
|
render: (_, record: SeriesItemType) => {
|
||||||
|
return <>{record.tags.SliceThickness}</>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "制造商",
|
||||||
|
dataIndex: "Manufacturer",
|
||||||
|
key: "Manufacturer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "成像方式",
|
||||||
|
dataIndex: "Modality",
|
||||||
|
key: "Modality",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "焦点大小",
|
||||||
|
// key: "FocalSpots",
|
||||||
|
// render: (_, record: SeriesItemType) => {
|
||||||
|
// return <>{record.tags.FocalSpots}</>;
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "图像位数",
|
||||||
|
// key: "BitsStored",
|
||||||
|
// render: (_, record: SeriesItemType) => {
|
||||||
|
// return <>{record.tags.BitsStored} bit</>;
|
||||||
|
// },
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,44 +1,62 @@
|
||||||
import { useDomain } from "@/hook/useDomain";
|
import { useDomain } from "@/hook/useDomain";
|
||||||
import { Button, Space, Table } from "antd";
|
import { Button, Space, Spin, Table, Tooltip, message } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { columnsForStudy, columnsForSeries } from "./columns";
|
import { columnsForStudy, columnsForSeries } from "./columns";
|
||||||
import { EyeOutlined } from "@ant-design/icons";
|
import { DesktopOutlined, FileZipOutlined } from "@ant-design/icons";
|
||||||
import { openOHIFViewer } from "../Upload/util";
|
import { openOHIFViewer } from "../Upload/util";
|
||||||
|
|
||||||
interface DicomListProps {
|
interface DicomListProps {
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataItemType = {
|
export type SeriesItemType = {
|
||||||
StudyDate: string;
|
BodyPartExamined: string;
|
||||||
StudyTime: string;
|
ContrastBolusAgent: string;
|
||||||
AccessionNumber: string;
|
ID: string;
|
||||||
|
ImageOrientationPatient: string;
|
||||||
|
Instances: string[];
|
||||||
|
Manufacturer: string;
|
||||||
Modality: string;
|
Modality: string;
|
||||||
PatientName: string;
|
OperatorsName: string;
|
||||||
PatientID: string;
|
ProtocolName: string;
|
||||||
PatientBirthDate: string;
|
SeriesDate: string;
|
||||||
PatientSex: string;
|
|
||||||
StudyInstanceUID: string;
|
|
||||||
SeriesInstanceUID: string;
|
|
||||||
CharacterSet: string;
|
|
||||||
SeriesDescription: string;
|
SeriesDescription: string;
|
||||||
|
SeriesInstanceUID: string;
|
||||||
|
SeriesNumber: string;
|
||||||
|
SeriesTime: string;
|
||||||
|
StationName: string;
|
||||||
|
Type: "study";
|
||||||
|
tags: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StudyItemType = {
|
||||||
|
AccessionNumber: string;
|
||||||
|
ID: string;
|
||||||
|
InstitutionName: string;
|
||||||
|
PatientBirthDate: string;
|
||||||
|
PatientID: string;
|
||||||
|
PatientName: string;
|
||||||
|
PatientSex: string;
|
||||||
|
ReferringPhysicianName: string;
|
||||||
|
StudyDate: string;
|
||||||
StudyID: string;
|
StudyID: string;
|
||||||
SeriesNumber: string | number;
|
StudyInstanceUID: string;
|
||||||
NumberSeriesRelatedInstances: string | number;
|
StudyTime: string;
|
||||||
NumberPatientRelatedInstances: string | number;
|
Type: "series";
|
||||||
NumberPatientRelatedSeries: string | number;
|
subs: SeriesItemType[];
|
||||||
subs: DataItemType[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DicomList = (props: DicomListProps) => {
|
export const DicomList = (props: DicomListProps) => {
|
||||||
const [dataSource, setDataSource] = useState<DataItemType[]>([]);
|
const [dataSource, setDataSource] = useState<StudyItemType[]>([]);
|
||||||
const [tableLoading, setTableLoading] = useState(false);
|
const [tableLoading, setTableLoading] = useState(false);
|
||||||
const { userDomainService } = useDomain();
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const { userDomainService, dicomDomainService } = useDomain();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableLoading(true);
|
setTableLoading(true);
|
||||||
userDomainService.findDicoms().then((result) => {
|
userDomainService.findDicoms().then((result) => {
|
||||||
const { data } = result;
|
const { data } = result;
|
||||||
setDataSource(data as DataItemType[]);
|
setDataSource(data as StudyItemType[]);
|
||||||
setTableLoading(false);
|
setTableLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -46,7 +64,7 @@ export const DicomList = (props: DicomListProps) => {
|
||||||
/**
|
/**
|
||||||
* 序列级别
|
* 序列级别
|
||||||
*/
|
*/
|
||||||
const expandedRowRenderForSeries = (record: DataItemType) => {
|
const expandedRowRenderForSeries = (record: StudyItemType) => {
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
rowKey="SeriesInstanceUID"
|
rowKey="SeriesInstanceUID"
|
||||||
|
@ -55,16 +73,22 @@ export const DicomList = (props: DicomListProps) => {
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
dataIndex: "operation",
|
dataIndex: "operation",
|
||||||
render: (_: any, recordSeries: DataItemType) => (
|
render: (_: any, recordSeries: SeriesItemType) => (
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip title="在线阅片">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="text"
|
||||||
size="small"
|
icon={<DesktopOutlined />}
|
||||||
icon={<EyeOutlined />}
|
|
||||||
onClick={() => onViewDicom(record, recordSeries)}
|
onClick={() => onViewDicom(record, recordSeries)}
|
||||||
>
|
/>
|
||||||
阅片
|
</Tooltip>
|
||||||
</Button>
|
<Tooltip title="下载原始影像 .zip文件">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<FileZipOutlined />}
|
||||||
|
onClick={() => onDownloadDicom(recordSeries)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -75,17 +99,61 @@ export const DicomList = (props: DicomListProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onViewDicom = (record: DataItemType, recordSeries: DataItemType) => {
|
const onViewDicom = (record: StudyItemType, recordSeries: SeriesItemType) => {
|
||||||
const { StudyInstanceUID } = record;
|
const { StudyInstanceUID } = record;
|
||||||
const { SeriesInstanceUID } = recordSeries;
|
const { SeriesInstanceUID } = recordSeries;
|
||||||
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
|
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDownloadDicom = ({ ID, Type }: SeriesItemType | StudyItemType) => {
|
||||||
|
setLoading(true);
|
||||||
|
dicomDomainService.downloadDicom({ ID, Type }).finally(() => {
|
||||||
|
messageApi.success("数据下载成功");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{contextHolder}
|
||||||
|
{loading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
background: "rgb(255 195 195 / 30%)",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
zIndex: 19940121,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
columns={columnsForStudy}
|
columns={[
|
||||||
|
...columnsForStudy,
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
dataIndex: "operation",
|
||||||
|
render: (_: any, record: StudyItemType) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="下载原始影像 .zip文件">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<FileZipOutlined />}
|
||||||
|
onClick={() => onDownloadDicom(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
loading={tableLoading}
|
loading={tableLoading}
|
||||||
expandable={{
|
expandable={{
|
||||||
expandedRowRender: expandedRowRenderForSeries,
|
expandedRowRender: expandedRowRenderForSeries,
|
||||||
|
|
|
@ -7,4 +7,4 @@ NACOS_NAMESPACE=56a3b295-f319-4ced-82b5-0df2e98cc541
|
||||||
NACOS_DATAID='test'
|
NACOS_DATAID='test'
|
||||||
NACOS_GROUP='DEFAULT_GROUP'
|
NACOS_GROUP='DEFAULT_GROUP'
|
||||||
# dicom配置,可以后期转移到nacos配置中心
|
# dicom配置,可以后期转移到nacos配置中心
|
||||||
PACS_URL=http://localhost:8042/dicom-web/
|
PACS_URL=http://localhost:8042
|
|
@ -27,6 +27,47 @@ const metaFormatter = (data) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StudyResourceType = {
|
||||||
|
ID: string;
|
||||||
|
MainDicomTags: {
|
||||||
|
AccessionNumber: string;
|
||||||
|
StudyDate: string;
|
||||||
|
StudyDescription: string;
|
||||||
|
StudyID: string;
|
||||||
|
StudyInstanceUID: string;
|
||||||
|
StudyTime: string;
|
||||||
|
};
|
||||||
|
PatientMainDicomTags: {
|
||||||
|
PatientBirthDate: string;
|
||||||
|
PatientID: string;
|
||||||
|
PatientName: string;
|
||||||
|
PatientSex: string;
|
||||||
|
};
|
||||||
|
Series: string[];
|
||||||
|
Type: 'Study';
|
||||||
|
};
|
||||||
|
|
||||||
|
type SeriesResourceType = {
|
||||||
|
ExpectedNumberOfInstances: number;
|
||||||
|
ID: string;
|
||||||
|
Instances: string[];
|
||||||
|
MainDicomTags: {
|
||||||
|
Manufacturer: string;
|
||||||
|
Modality: string;
|
||||||
|
NumberOfSlices: string;
|
||||||
|
ProtocolName: string;
|
||||||
|
SeriesDate: string;
|
||||||
|
SeriesDescription: string;
|
||||||
|
SeriesInstanceUID: string;
|
||||||
|
SeriesNumber: string;
|
||||||
|
SeriesTime: string;
|
||||||
|
StationName: string;
|
||||||
|
};
|
||||||
|
ParentStudy: string;
|
||||||
|
Status: string;
|
||||||
|
Type: 'Series';
|
||||||
|
};
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -38,19 +79,52 @@ export class AppController {
|
||||||
async findDicoms() {
|
async findDicoms() {
|
||||||
const pacsUrl = this.configService.get('PACS_URL');
|
const pacsUrl = this.configService.get('PACS_URL');
|
||||||
try {
|
try {
|
||||||
const { data: studyData } = await axios.get(`${pacsUrl}/studies`);
|
// 全部的study pacs uuid
|
||||||
|
const { data: study_uuids } = await axios.get(`${pacsUrl}/studies`);
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const study of studyData) {
|
// study
|
||||||
const studyItem = metaFormatter(study);
|
for (const study_uuid of study_uuids) {
|
||||||
const { data: seriesData } = await axios.get(
|
const studyUrl = `${pacsUrl}/studies/${study_uuid}`;
|
||||||
`${pacsUrl}/studies/${studyItem.StudyInstanceUID}/series`,
|
const { data: studyResource } = await axios.get(studyUrl);
|
||||||
);
|
const { Series, PatientMainDicomTags, MainDicomTags, ID, Type } =
|
||||||
const subs = seriesData.map((s) => metaFormatter(s));
|
studyResource as StudyResourceType;
|
||||||
result.push({ ...studyItem, subs });
|
const subs = [];
|
||||||
|
// series + 第一张 instance 的 tags
|
||||||
|
for (const series_uuid of Series) {
|
||||||
|
const seriesUrl = `${pacsUrl}/series/${series_uuid}`;
|
||||||
|
const { data: seriesResource } = await axios.get(seriesUrl);
|
||||||
|
const { MainDicomTags, Instances, ID, Type } =
|
||||||
|
seriesResource as SeriesResourceType;
|
||||||
|
// 取第一张instance
|
||||||
|
const InstanceNumber = Instances.length;
|
||||||
|
const firstInstanceUrl = `${pacsUrl}/instances/${Instances[0]}/simplified-tags`;
|
||||||
|
const tags =
|
||||||
|
Instances.length > 0
|
||||||
|
? await axios.get(firstInstanceUrl).then((d) => d.data)
|
||||||
|
: {};
|
||||||
|
subs.push({ ...MainDicomTags, Type, InstanceNumber, ID, tags });
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
...PatientMainDicomTags,
|
||||||
|
...MainDicomTags,
|
||||||
|
Type,
|
||||||
|
ID,
|
||||||
|
subs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { data: result };
|
return { data: result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to fetch DICOM data: ${error.message}`);
|
throw new Error(`Failed to fetch DICOM data: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventPattern({ cmd: 'dicom.archive.url' })
|
||||||
|
async archiveUrl({ ID, Type }: { ID: string; Type: string }) {
|
||||||
|
const pacsUrl = this.configService.get('PACS_URL');
|
||||||
|
const mapping = {
|
||||||
|
study: 'studies',
|
||||||
|
series: 'series',
|
||||||
|
};
|
||||||
|
return `${pacsUrl}/${mapping[Type.toLowerCase()]}/${ID}/archive`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
"class-validator": "0.14.0",
|
"class-validator": "0.14.0",
|
||||||
"class-transformer": "0.5.1",
|
"class-transformer": "0.5.1",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"dayjs": "1.11.9"
|
"dayjs": "1.11.9",
|
||||||
|
"axios": "1.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { ForbiddenExceptionFilter } from './filter/forbid.filter';
|
||||||
import { AuthController } from './auth/auth.controller';
|
import { AuthController } from './auth/auth.controller';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AnnotatorModule } from './annotator/annotator.module';
|
import { AnnotatorModule } from './annotator/annotator.module';
|
||||||
|
import { DicomModule } from './dicom/dicom.module';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
@ -30,6 +31,7 @@ import * as cookieParser from 'cookie-parser';
|
||||||
]),
|
]),
|
||||||
AdminModule,
|
AdminModule,
|
||||||
AnnotatorModule,
|
AnnotatorModule,
|
||||||
|
DicomModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController, AuthController],
|
controllers: [AppController, AuthController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
28
apps/services/dmp/gateway/src/dicom/dicom.controller.ts
Normal file
28
apps/services/dmp/gateway/src/dicom/dicom.controller.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Body, Controller, Get, Inject, Param, Post, Res } from '@nestjs/common';
|
||||||
|
import { ClientProxy } from '@nestjs/microservices';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Controller('dicom')
|
||||||
|
export class DicomController {
|
||||||
|
constructor(@Inject('Client') private client: ClientProxy) {}
|
||||||
|
|
||||||
|
@Post('download')
|
||||||
|
async downloadDicom(@Body() body, @Res() res: Response) {
|
||||||
|
const { ID, Type } = body;
|
||||||
|
const url = await firstValueFrom(
|
||||||
|
this.client.send({ cmd: 'dicom.archive.url' }, { ID, Type }),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const { data: dataStream } = await axios.get(url, {
|
||||||
|
responseType: 'stream',
|
||||||
|
});
|
||||||
|
res.setHeader('Content-Name', `${ID}.zip`);
|
||||||
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
|
dataStream.pipe(res);
|
||||||
|
} catch (error) {
|
||||||
|
return { code: 1, msg: error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
apps/services/dmp/gateway/src/dicom/dicom.module.ts
Normal file
21
apps/services/dmp/gateway/src/dicom/dicom.module.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DicomController } from './dicom.controller';
|
||||||
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ClientsModule.register([
|
||||||
|
{
|
||||||
|
name: 'Client',
|
||||||
|
transport: Transport.NATS,
|
||||||
|
options: {
|
||||||
|
servers: ['nats://localhost:4222'],
|
||||||
|
maxReconnectAttempts: 5,
|
||||||
|
reconnectTimeWait: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [DicomController],
|
||||||
|
})
|
||||||
|
export class DicomModule {}
|
|
@ -258,6 +258,9 @@ importers:
|
||||||
dicom-parser:
|
dicom-parser:
|
||||||
specifier: 1.8.21
|
specifier: 1.8.21
|
||||||
version: 1.8.21
|
version: 1.8.21
|
||||||
|
file-saver:
|
||||||
|
specifier: 2.0.5
|
||||||
|
version: registry.npmmirror.com/file-saver@2.0.5
|
||||||
mobx:
|
mobx:
|
||||||
specifier: 6.9.0
|
specifier: 6.9.0
|
||||||
version: 6.9.0
|
version: 6.9.0
|
||||||
|
@ -283,6 +286,9 @@ importers:
|
||||||
specifier: 6.14.2
|
specifier: 6.14.2
|
||||||
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
|
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/file-saver':
|
||||||
|
specifier: 2.0.5
|
||||||
|
version: registry.npmmirror.com/@types/file-saver@2.0.5
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.14
|
specifier: 18.2.14
|
||||||
version: 18.2.14
|
version: 18.2.14
|
||||||
|
@ -1073,6 +1079,9 @@ importers:
|
||||||
'@nestjs/typeorm':
|
'@nestjs/typeorm':
|
||||||
specifier: 10.0.0
|
specifier: 10.0.0
|
||||||
version: 10.0.0(@nestjs/common@10.1.0)(@nestjs/core@10.1.0)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17)
|
version: 10.0.0(@nestjs/common@10.1.0)(@nestjs/core@10.1.0)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17)
|
||||||
|
axios:
|
||||||
|
specifier: 1.5.0
|
||||||
|
version: registry.npmmirror.com/axios@1.5.0
|
||||||
class-transformer:
|
class-transformer:
|
||||||
specifier: 0.5.1
|
specifier: 0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
|
@ -12778,6 +12787,12 @@ packages:
|
||||||
'@types/serve-static': registry.npmmirror.com/@types/serve-static@1.15.2
|
'@types/serve-static': registry.npmmirror.com/@types/serve-static@1.15.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/file-saver@2.0.5:
|
||||||
|
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.5.tgz}
|
||||||
|
name: '@types/file-saver'
|
||||||
|
version: 2.0.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
registry.npmmirror.com/@types/http-errors@2.0.1:
|
registry.npmmirror.com/@types/http-errors@2.0.1:
|
||||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.1.tgz}
|
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.1.tgz}
|
||||||
name: '@types/http-errors'
|
name: '@types/http-errors'
|
||||||
|
@ -14869,6 +14884,12 @@ packages:
|
||||||
flat-cache: registry.npmmirror.com/flat-cache@3.0.4
|
flat-cache: registry.npmmirror.com/flat-cache@3.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/file-saver@2.0.5:
|
||||||
|
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz}
|
||||||
|
name: file-saver
|
||||||
|
version: 2.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/fill-range@7.0.1:
|
registry.npmmirror.com/fill-range@7.0.1:
|
||||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz}
|
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz}
|
||||||
name: fill-range
|
name: fill-range
|
||||||
|
|
Loading…
Reference in New Issue
Block a user