feat: 上传全部影像
This commit is contained in:
parent
976d687b1b
commit
184d84726e
|
@ -54,7 +54,6 @@ export class UserService {
|
|||
* 分配标注序列
|
||||
*/
|
||||
async createArchiveTask(user: User, study: Study[]) {
|
||||
console.log(user, study);
|
||||
return await this.userRepository.createArchiveTask({ user, study });
|
||||
}
|
||||
}
|
||||
|
|
43
apps/dmp/src/modules/Admin/Dicom/Upload/AssignModal.tsx
Normal file
43
apps/dmp/src/modules/Admin/Dicom/Upload/AssignModal.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import { Modal, Select } from "antd";
|
||||
|
||||
interface AssignModalProps {
|
||||
isOpen: boolean;
|
||||
options: { value: unknown; label: unknown }[];
|
||||
value: number | undefined;
|
||||
onSelectAnnotator: (id: number) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const AssignModal: React.FC<AssignModalProps> = ({
|
||||
isOpen,
|
||||
options,
|
||||
value,
|
||||
onSelectAnnotator,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
width={320}
|
||||
title="选择标注"
|
||||
open={isOpen}
|
||||
closable
|
||||
cancelText="再想想"
|
||||
okText="确定"
|
||||
onOk={onConfirm}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%", marginBottom: 20 }}
|
||||
placeholder="选择标注"
|
||||
value={value}
|
||||
onChange={(id) => onSelectAnnotator(id)}
|
||||
options={options}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignModal;
|
25
apps/dmp/src/modules/Admin/Dicom/Upload/DicomFileInfo.tsx
Normal file
25
apps/dmp/src/modules/Admin/Dicom/Upload/DicomFileInfo.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Typography, Space, Divider } from "antd";
|
||||
import React from "react";
|
||||
|
||||
interface DicomFileInfoProps {
|
||||
totalFileNum: string | number;
|
||||
dcmFileNum: string | number;
|
||||
dcmFileSize: string | number;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DicomFileInfo: React.FC<DicomFileInfoProps> = ({
|
||||
totalFileNum,
|
||||
dcmFileNum,
|
||||
dcmFileSize,
|
||||
}) =>
|
||||
!!totalFileNum && (
|
||||
<Space>
|
||||
<Text type="secondary">扫描总文件数: {totalFileNum}</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">包含: {dcmFileNum} 个dicom文件</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
|
||||
</Space>
|
||||
);
|
|
@ -2,60 +2,60 @@ import { TableColumnsType } from "antd";
|
|||
import { Series, Study } from "../DicomUploader/util";
|
||||
|
||||
export const columnsForStudy: TableColumnsType<Study> = [
|
||||
{ title: "病历号", dataIndex: "patientID", key: "patientID" },
|
||||
{ title: "姓名", dataIndex: "patientName", key: "patientName" },
|
||||
{ title: "病历号", dataIndex: "PatientID", key: "PatientID" },
|
||||
{ title: "姓名", dataIndex: "PatientName", key: "PatientName" },
|
||||
{
|
||||
title: "序列数",
|
||||
key: "seriesNumber",
|
||||
key: "SeriesNumber",
|
||||
render: (_: any, record: Study) => <span>{record.subs.length}</span>,
|
||||
},
|
||||
{
|
||||
title: "切片数量",
|
||||
key: "seriesNumber",
|
||||
key: "SliceNumber",
|
||||
render: (_: any, record: Study) => (
|
||||
<span>
|
||||
{record.subs.map((s) => s.subs.length).reduce((p, n) => p + n)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ title: "年龄", dataIndex: "patientAge", key: "patientAge" },
|
||||
{ title: "性别", dataIndex: "patientSex", key: "patientSex" },
|
||||
{ title: "年龄", dataIndex: "PatientAge", key: "PatientAge" },
|
||||
{ title: "性别", dataIndex: "PatientSex", key: "PatientSex" },
|
||||
{
|
||||
title: "扫描日期",
|
||||
dataIndex: "acquisitionDate",
|
||||
key: "acquisitionDate",
|
||||
dataIndex: "AcquisitionDate",
|
||||
key: "AcquisitionDate",
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "studyDescription",
|
||||
key: "studyDescription",
|
||||
dataIndex: "StudyDescription",
|
||||
key: "StudyDescription",
|
||||
},
|
||||
];
|
||||
|
||||
export const columnsForSeries: TableColumnsType<Series> = [
|
||||
{
|
||||
title: "序列描述",
|
||||
dataIndex: "seriesDescription",
|
||||
key: "seriesDescription",
|
||||
dataIndex: "SeriesDescription",
|
||||
key: "SeriesDescription",
|
||||
},
|
||||
{
|
||||
title: "切片数量",
|
||||
key: "sliceNumber",
|
||||
key: "SliceNumber",
|
||||
render: (_: any, record: Series) => <span>{record.subs.length}</span>,
|
||||
},
|
||||
{
|
||||
title: "切片层厚",
|
||||
dataIndex: "sliceThickness",
|
||||
key: "sliceThickness",
|
||||
dataIndex: "SliceThickness",
|
||||
key: "SliceThickness",
|
||||
},
|
||||
{
|
||||
title: "成像设备",
|
||||
dataIndex: "modality",
|
||||
key: "modality",
|
||||
dataIndex: "Modality",
|
||||
key: "Modality",
|
||||
},
|
||||
{
|
||||
title: "设备制造商",
|
||||
dataIndex: "manufacturer",
|
||||
key: "manufacturer",
|
||||
dataIndex: "Manufacturer",
|
||||
key: "Manufacturer",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -2,27 +2,27 @@ import * as dicomParser from "dicom-parser";
|
|||
|
||||
export interface Series {
|
||||
SeriesInstanceUID: string;
|
||||
sliceThickness: string;
|
||||
seriesDescription: string;
|
||||
SliceThickness: string;
|
||||
SeriesDescription: string;
|
||||
/**
|
||||
* 成像设备
|
||||
*/
|
||||
modality: string;
|
||||
Modality: string;
|
||||
/**
|
||||
* 设备商
|
||||
*/
|
||||
manufacturer: string;
|
||||
Manufacturer: string;
|
||||
subs: File[];
|
||||
}
|
||||
|
||||
export interface Study {
|
||||
StudyInstanceUID: string;
|
||||
studyDescription: string;
|
||||
patientID: string;
|
||||
patientAge: string;
|
||||
patientName: string;
|
||||
patientSex: string;
|
||||
acquisitionDate: string;
|
||||
StudyDescription: string;
|
||||
PatientID: string;
|
||||
PatientAge: string;
|
||||
PatientName: string;
|
||||
PatientSex: string;
|
||||
AcquisitionDate: string;
|
||||
subs: Series[];
|
||||
}
|
||||
|
||||
|
@ -38,31 +38,31 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
|
|||
const byteArray = new Uint8Array(arrayBuffer);
|
||||
const dataSet = dicomParser.parseDicom(byteArray);
|
||||
|
||||
const patientID = dataSet.string("x00100020") ?? "无";
|
||||
const PatientID = dataSet.string("x00100020") ?? "无";
|
||||
const StudyInstanceUID = dataSet.string("x0020000d");
|
||||
const studyDescription = dataSet.string("x00081030") ?? "无";
|
||||
const StudyDescription = dataSet.string("x00081030") ?? "无";
|
||||
const SeriesInstanceUID = dataSet.string("x0020000e");
|
||||
const patientName = dataSet.string("x00100010") ?? "无";
|
||||
const patientSex = dataSet.string("x00100040") ?? "无";
|
||||
const patientAge = dataSet.string("x00101010") ?? "无";
|
||||
const sliceThickness = dataSet.string("x00180050");
|
||||
const acquisitionDate = dataSet.string("x00080022") ?? "无";
|
||||
const seriesDescription = dataSet.string("x0008103e") ?? "无";
|
||||
const modality = dataSet.string("x00080060") ?? "无";
|
||||
const manufacturer = dataSet.string("x00081090") ?? "无";
|
||||
const PatientName = dataSet.string("x00100010") ?? "无";
|
||||
const PatientSex = dataSet.string("x00100040") ?? "无";
|
||||
const PatientAge = dataSet.string("x00101010") ?? "无";
|
||||
const SliceThickness = dataSet.string("x00180050");
|
||||
const AcquisitionDate = dataSet.string("x00080022") ?? "无";
|
||||
const SeriesDescription = dataSet.string("x0008103e") ?? "无";
|
||||
const Modality = dataSet.string("x00080060") ?? "无";
|
||||
const Manufacturer = dataSet.string("x00081090") ?? "无";
|
||||
|
||||
if (StudyInstanceUID && SeriesInstanceUID && sliceThickness) {
|
||||
if (StudyInstanceUID && SeriesInstanceUID && SliceThickness) {
|
||||
// 病例级别
|
||||
let study = studys.find((s) => s.StudyInstanceUID === StudyInstanceUID);
|
||||
if (!study) {
|
||||
study = {
|
||||
StudyInstanceUID,
|
||||
studyDescription,
|
||||
patientID,
|
||||
patientName,
|
||||
patientSex,
|
||||
patientAge,
|
||||
acquisitionDate,
|
||||
StudyDescription,
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
AcquisitionDate,
|
||||
subs: [],
|
||||
};
|
||||
studys.push(study);
|
||||
|
@ -74,10 +74,10 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
|
|||
if (!series) {
|
||||
series = {
|
||||
SeriesInstanceUID,
|
||||
sliceThickness,
|
||||
seriesDescription,
|
||||
modality,
|
||||
manufacturer,
|
||||
SliceThickness,
|
||||
SeriesDescription,
|
||||
Modality,
|
||||
Manufacturer,
|
||||
subs: [],
|
||||
};
|
||||
study.subs.push(series);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import React from "react";
|
||||
import { Modal, Progress } from "antd";
|
||||
|
||||
interface UploadProgressProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export const UploadProgressModal: React.FC<UploadProgressProps> = ({
|
||||
isOpen,
|
||||
percent,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<Modal title={title} open={isOpen} footer={null} closable={false}>
|
||||
<Progress percent={percent} strokeLinecap="butt" />
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,26 +1,16 @@
|
|||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Modal,
|
||||
Progress,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Button, Col, Row, Space, message } from "antd";
|
||||
import { useDicomUploader } from "./DicomUploader";
|
||||
import { Series, Study } from "./DicomUploader/util";
|
||||
import { DicomTable } from "./DicomTable";
|
||||
import { useDomain } from "@/hook/useDomain";
|
||||
import { Typography } from "antd";
|
||||
import { useState } from "react";
|
||||
import { User } from "@@/domain/User/entities/User";
|
||||
import { limitConcurrency } from "./limitConcurrency";
|
||||
import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons";
|
||||
import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util";
|
||||
|
||||
const { Text } = Typography;
|
||||
import { DicomFileInfo } from "./DicomFileInfo";
|
||||
import { UploadProgressModal } from "./UploadProgressModal";
|
||||
import AssignModal from "./AssignModal";
|
||||
|
||||
interface DicomUploadProps {
|
||||
children?: JSX.Element;
|
||||
|
@ -33,6 +23,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
|
||||
const [selectRows, setSelectedRows] = useState<Study[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAssignLoading, setIsAssignLoading] = useState(false);
|
||||
const [annotators, setAnnotators] = useState<User[]>([]);
|
||||
const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>(
|
||||
undefined
|
||||
|
@ -72,9 +63,9 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* 上传全部
|
||||
* 返回不在pacs中的影像数据
|
||||
*/
|
||||
const onUploadEntiredDicom = () => {
|
||||
const filterDicomPromise = () => {
|
||||
const items = flatternStudies(studys);
|
||||
// 过滤存在pacs中的序列
|
||||
const checkExistPromises = items.map(
|
||||
|
@ -88,8 +79,15 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
resolve(existInPacs ? false : item);
|
||||
})
|
||||
) as Promise<FlatStudyItem | false>[];
|
||||
return Promise.all(checkExistPromises);
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传全部
|
||||
*/
|
||||
const onUploadEntiredDicom = () => {
|
||||
// 上传到pacs
|
||||
Promise.all(checkExistPromises).then((result) => {
|
||||
filterDicomPromise().then((result) => {
|
||||
const files = result.map((i) => (!i ? [] : i.Files)).flat();
|
||||
if (files.length === 0) return messageApi.info("全部影像均已存在pacs中");
|
||||
setVisible(true);
|
||||
|
@ -109,26 +107,22 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
};
|
||||
|
||||
const onClickAssign = () => {
|
||||
userDomainService.getDmpAnnotators().then((res) => {
|
||||
setIsModalOpen(true);
|
||||
setAnnotators(res.data as User[]);
|
||||
// 检查是否全部传到pacs
|
||||
setIsAssignLoading(true);
|
||||
filterDicomPromise().then((result) => {
|
||||
const files = result.map((i) => (!i ? [] : i.Files)).flat();
|
||||
if (files.length !== 0) {
|
||||
setIsAssignLoading(false);
|
||||
return messageApi.info("请先上传全部影像到pacs");
|
||||
}
|
||||
userDomainService.getDmpAnnotators().then((res) => {
|
||||
setIsAssignLoading(false);
|
||||
setIsModalOpen(true);
|
||||
setAnnotators(res.data as User[]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 统计信息
|
||||
*/
|
||||
const DicomFileInfo = () =>
|
||||
!!totalFileNum && (
|
||||
<Space>
|
||||
<Text type="secondary">扫描总文件数: {totalFileNum}</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">包含: {dcmFileNum} 个dicom文件</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
|
||||
</Space>
|
||||
);
|
||||
|
||||
/**
|
||||
* 分配任务
|
||||
*/
|
||||
|
@ -157,6 +151,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
icon={<InboxOutlined />}
|
||||
disabled={selectRows.length === 0}
|
||||
type="primary"
|
||||
loading={isAssignLoading}
|
||||
onClick={onClickAssign}
|
||||
>
|
||||
分配
|
||||
|
@ -164,39 +159,35 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
<DicomFileInfo />
|
||||
<DicomFileInfo
|
||||
dcmFileNum={dcmFileNum}
|
||||
dcmFileSize={dcmFileSize}
|
||||
totalFileNum={totalFileNum}
|
||||
/>
|
||||
<DicomTable
|
||||
studys={studys}
|
||||
loading={isLoading}
|
||||
onSelectedRows={(rows) => setSelectedRows(rows)}
|
||||
onUploadFiles={onUploadFiles}
|
||||
/>
|
||||
<Modal
|
||||
width={320}
|
||||
title="选择标注"
|
||||
open={isModalOpen}
|
||||
closable
|
||||
cancelText="再想想"
|
||||
okText="确定"
|
||||
onOk={onAssignConfirm}
|
||||
<AssignModal
|
||||
isOpen={isModalOpen}
|
||||
options={annotators.map((a) => ({
|
||||
value: a.id,
|
||||
label: a.username,
|
||||
}))}
|
||||
value={selectAnnotator?.id as number}
|
||||
onSelectAnnotator={(id: number) =>
|
||||
setSelectAnnotator(annotators.find((a) => a.id === id))
|
||||
}
|
||||
onConfirm={onAssignConfirm}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%", marginBottom: 20 }}
|
||||
placeholder="选择标注"
|
||||
value={selectAnnotator?.id as number}
|
||||
onChange={(id: number) =>
|
||||
setSelectAnnotator(annotators.find((a) => a.id === id))
|
||||
}
|
||||
options={annotators.map((a) => ({
|
||||
value: a.id,
|
||||
label: a.username,
|
||||
}))}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal title="上传影像中" open={visible} footer={null} closable={false}>
|
||||
<Progress percent={percent} strokeLinecap="butt" />
|
||||
</Modal>
|
||||
/>
|
||||
<UploadProgressModal
|
||||
title="上传影像中"
|
||||
isOpen={visible}
|
||||
percent={percent}
|
||||
/>
|
||||
{contextHolder}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,8 +8,6 @@ export class AppController {
|
|||
|
||||
@EventPattern({ cmd: 'archive.task.create' })
|
||||
async createArchiveTask(payload) {
|
||||
const { annotatorId, study } = payload;
|
||||
console.log(study);
|
||||
return 123;
|
||||
return await this.appService.createArchiveTask(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { AppService } from './app.service';
|
|||
import { NacosModule } from './nacos/nacos.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ArchiveTask } from './entity/archiveTask';
|
||||
import { ArchiveTask } from './entity/archiveTask.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -19,7 +19,7 @@ import { ArchiveTask } from './entity/archiveTask';
|
|||
port: 3306,
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
database: 'dicom',
|
||||
database: 'dmp',
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
timezone: 'Asia/Shanghai', // 这里设置了时区
|
||||
|
|
|
@ -1,4 +1,32 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ArchiveTask } from './entity/archiveTask.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {}
|
||||
export class AppService {
|
||||
constructor(
|
||||
@InjectRepository(ArchiveTask)
|
||||
private readonly archiveTaskRepository: Repository<ArchiveTask>,
|
||||
) {}
|
||||
async createArchiveTask(payload) {
|
||||
const { annotatorId, study } = payload;
|
||||
try {
|
||||
for (let i = 0; i < study.length; i++) {
|
||||
const { StudyInstanceUID, PatientID } = study[i];
|
||||
for (let j = 0; j < study[i].subs.length; j++) {
|
||||
const { SeriesInstanceUID } = study[i].subs[j];
|
||||
await this.archiveTaskRepository.save({
|
||||
annotatorId,
|
||||
PatientID,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@ import {
|
|||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@Unique(['annotatorId', 'StudyInstanceUID', 'SeriesInstanceUID'])
|
||||
export class ArchiveTask {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
@ -14,7 +17,10 @@ export class ArchiveTask {
|
|||
annotatorId: number;
|
||||
|
||||
@Column()
|
||||
StudyInstanceUID;
|
||||
PatientID: string;
|
||||
|
||||
@Column()
|
||||
StudyInstanceUID: string;
|
||||
|
||||
@Column()
|
||||
SeriesInstanceUID: string;
|
|
@ -17,10 +17,10 @@ export class AdminController {
|
|||
@Post('createArchiveTask')
|
||||
async createArchiveTask(@Body() body) {
|
||||
const { user, study } = body;
|
||||
const { annotatorId } = user;
|
||||
const { data } = await firstValueFrom(
|
||||
const { id: annotatorId } = user;
|
||||
const { success, error } = await firstValueFrom(
|
||||
this.client.send({ cmd: 'archive.task.create' }, { annotatorId, study }),
|
||||
);
|
||||
return { data, code: 0 };
|
||||
return success ? { code: 0 } : { code: 1, msg: error.code };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user