feat: 批量上传pacs

This commit is contained in:
mozzie 2023-09-19 17:10:17 +08:00
parent d60469dae8
commit 6f62e35ff3
12 changed files with 65 additions and 219 deletions

View File

@ -1,5 +1,6 @@
import { Apis, ArchiveTaskCreateDto } from "@@/infra/api"; import { Apis } from "@@/infra/api";
import { User } from "./entities/User"; import { User } from "./entities/User";
import { ArchiveTaskCreateDto } from "@@/infra/api/dto";
export class UserRepository { export class UserRepository {
async authLogin(user: Pick<User, "username" | "password">) { async authLogin(user: Pick<User, "username" | "password">) {

View File

@ -1,5 +1,5 @@
import { columnsForSeries, columnsForStudy } from "./columns"; import { columnsForSeries, columnsForStudy } from "./columns";
import { EyeOutlined, TagOutlined } from "@ant-design/icons"; import { DesktopOutlined, EyeOutlined, TagOutlined } from "@ant-design/icons";
import { Series, Study } from "../DicomUploader/util"; import { Series, Study } from "../DicomUploader/util";
import { Button, Space, Table, Tooltip } from "antd"; import { Button, Space, Table, Tooltip } from "antd";
interface DicomTableProps { interface DicomTableProps {
@ -26,14 +26,13 @@ export const DicomTable = (props: DicomTableProps) => {
dataIndex: "operation", dataIndex: "operation",
render: (_: any, recordSeries: Series) => ( render: (_: any, recordSeries: Series) => (
<Space> <Space>
<Button <Tooltip title="在线阅片">
type="primary" <Button
size="small" type="text"
icon={<EyeOutlined />} icon={<DesktopOutlined />}
onClick={() => props.onUploadFiles?.(record, recordSeries)} onClick={() => props.onUploadFiles?.(record, recordSeries)}
> />
</Tooltip>
</Button>
</Space> </Space>
), ),
}, },

View File

@ -50,7 +50,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
series.subs.map((f: File) => () => dicomDomainService.upload2Pacs(f)), series.subs.map((f: File) => () => dicomDomainService.upload2Pacs(f)),
10, 10,
(completed, total) => { (completed, total) => {
console.log(`${completed} out of ${total} tasks completed.`); // console.log(`${completed} out of ${total} tasks completed.`);
setPercent(Math.floor((completed / total) * 100)); setPercent(Math.floor((completed / total) * 100));
}, },
() => { () => {
@ -63,22 +63,25 @@ export const DicomUpload = (props: DicomUploadProps) => {
}; };
/** /**
* pacs中的影像数据 * pacs中的影像数据
*/ */
const filterDicomPromise = () => { const filterOutPACSDicom = () => {
const items = flatternStudies(studys); const items = flatternStudies(studys);
// 过滤存在pacs中的序列 // 过滤存在pacs中的序列
const checkExistPromises = items.map( const checkExistPromises = items.map(
(item) => (item) =>
new Promise(async (resolve) => { new Promise((resolve) => {
const instances = await dicomDomainService.existInPacs({ dicomDomainService
SeriesInstanceUID: item.SeriesInstanceUID, .existInPacs({
StudyInstanceUID: item.StudyInstanceUID, SeriesInstanceUID: item.SeriesInstanceUID,
}); StudyInstanceUID: item.StudyInstanceUID,
const existInPacs = instances.length === item.Files.length; })
resolve(existInPacs ? false : item); .then((instances) => {
const existInPacs = instances.length === item.Files.length;
resolve(existInPacs ? false : item);
});
}) })
) as Promise<FlatStudyItem | false>[]; ) as unknown as FlatStudyItem[];
return Promise.all(checkExistPromises); return Promise.all(checkExistPromises);
}; };
@ -87,15 +90,18 @@ export const DicomUpload = (props: DicomUploadProps) => {
*/ */
const onUploadEntiredDicom = () => { const onUploadEntiredDicom = () => {
// 上传到pacs // 上传到pacs
filterDicomPromise().then((result) => { filterOutPACSDicom().then((result) => {
const files = result.map((i) => (!i ? [] : i.Files)).flat(); const files = result
.filter(Boolean)
.map((i) => i.Files)
.flat();
if (files.length === 0) return messageApi.info("全部影像均已存在pacs中"); if (files.length === 0) return messageApi.info("全部影像均已存在pacs中");
setVisible(true); setVisible(true);
limitConcurrency( limitConcurrency(
files.map((f: File) => () => dicomDomainService.upload2Pacs(f)), files.map((f: File) => () => dicomDomainService.upload2Pacs(f)),
10, 10,
(completed, total) => { (completed, total) => {
console.log(`${completed} out of ${total} tasks completed.`); // console.log(`${completed} out of ${total} tasks completed.`);
setPercent(Math.floor((completed / total) * 100)); setPercent(Math.floor((completed / total) * 100));
}, },
() => { () => {
@ -109,8 +115,11 @@ export const DicomUpload = (props: DicomUploadProps) => {
const onClickAssign = () => { const onClickAssign = () => {
// 检查是否全部传到pacs // 检查是否全部传到pacs
setIsAssignLoading(true); setIsAssignLoading(true);
filterDicomPromise().then((result) => { filterOutPACSDicom().then((result) => {
const files = result.map((i) => (!i ? [] : i.Files)).flat(); const files = result
.filter(Boolean)
.map((i) => i.Files)
.flat();
if (files.length !== 0) { if (files.length !== 0) {
setIsAssignLoading(false); setIsAssignLoading(false);
return messageApi.info("请先上传全部影像到pacs"); return messageApi.info("请先上传全部影像到pacs");

View File

@ -1,5 +1,13 @@
type PromiseFunction = () => Promise<any>; type PromiseFunction = () => Promise<any>;
/**
* pacs
* @param tasks
* @param maxConcurrency
* @param onProgress
* @param onComplete
* @returns
*/
export async function limitConcurrency( export async function limitConcurrency(
tasks: PromiseFunction[], tasks: PromiseFunction[],
maxConcurrency: number, maxConcurrency: number,

View File

@ -13,11 +13,22 @@ type CategoryItemType = {
labels: { name: string; id: number }[]; labels: { name: string; id: number }[];
}; };
type ArchiveItemType = {
id: number;
username: string;
PatientID: string;
StudyInstanceUID: string;
SeriesInstanceUID: string;
createTime: string;
updateTime: string;
};
export const ArchiveList = (props: ArchiveListProps) => { export const ArchiveList = (props: ArchiveListProps) => {
const [dataSource, setDataSource] = useState<CategoryItemType[]>([]); const [dataSource, setDataSource] = useState<ArchiveItemType[]>([]);
const [tableLoading, setTableLoading] = useState(false); const [tableLoading, setTableLoading] = useState(false);
const { userDomainService, labelDomainService } = useDomain(); const { userDomainService, labelDomainService } = useDomain();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [selectLabels, setSelectLabels] = useState<number[]>();
const [labelOptions, setLabelOptions] = useState< const [labelOptions, setLabelOptions] = useState<
{ label: string; value: string }[] { label: string; value: string }[]
>([]); >([]);
@ -26,7 +37,7 @@ export const ArchiveList = (props: ArchiveListProps) => {
userDomainService.findArchiveTask().then((res) => { userDomainService.findArchiveTask().then((res) => {
const { code, data } = res; const { code, data } = res;
if (code === 0) { if (code === 0) {
setDataSource(data); setDataSource(data as ArchiveItemType[]);
} }
setTableLoading(false); setTableLoading(false);
}); });
@ -43,29 +54,21 @@ export const ArchiveList = (props: ArchiveListProps) => {
setLabelOptions(labels); setLabelOptions(labels);
} }
}); });
}, []); }, [labelDomainService]);
const onViewDicom = (record: any) => { const onViewDicom = (record: ArchiveItemType) => {
const { StudyInstanceUID, SeriesInstanceUID } = record; const { StudyInstanceUID, SeriesInstanceUID } = record;
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID); openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
}; };
const onLabelSelectChange = (labelIds: string[]) => { const onLabelSelectChange = (labelIds: string[]) =>
console.log( setSelectLabels(labelIds.map((i) => Number(i)));
"选中的labelIds:",
labelIds.map((i) => Number(i))
);
};
const onUpdateLabelForSeries = () => { const onUpdateLabelForSeries = () => {
console.log("onUpdateLabelForSeries"); if (selectLabels?.length === 0) return setIsModalOpen(false);
console.log("onUpdateLabelForSeries", selectLabels);
}; };
const filterOption = (
input: string,
option: { label: string; value: string }
) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase());
return ( return (
<div> <div>
<Table <Table
@ -76,7 +79,7 @@ export const ArchiveList = (props: ArchiveListProps) => {
{ {
title: "操作", title: "操作",
dataIndex: "operation", dataIndex: "operation",
render: (_: any, record: any) => ( render: (_: any, record: ArchiveItemType) => (
<Space> <Space>
<Tooltip title="在线阅片"> <Tooltip title="在线阅片">
<Button <Button
@ -114,8 +117,7 @@ export const ArchiveList = (props: ArchiveListProps) => {
defaultValue={[]} defaultValue={[]}
onChange={onLabelSelectChange} onChange={onLabelSelectChange}
options={labelOptions} options={labelOptions}
optionFilterProp="children" optionFilterProp="label"
filterOption={filterOption}
/> />
</Modal> </Modal>
</div> </div>

View File

@ -3,7 +3,6 @@ import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { NacosModule } from './nacos/nacos.module'; import { NacosModule } from './nacos/nacos.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { RetrievalModule } from './retrieval/retrieval.module';
import { LabelModule } from './label/label.module'; import { LabelModule } from './label/label.module';
import { APP_FILTER } from '@nestjs/core'; import { APP_FILTER } from '@nestjs/core';
import { TypeOrmExceptionFilter } from './filter/orm.exception.filter'; import { TypeOrmExceptionFilter } from './filter/orm.exception.filter';
@ -15,7 +14,6 @@ import { TypeOrmExceptionFilter } from './filter/orm.exception.filter';
envFilePath: `.env.${process.env.NODE_ENV}`, envFilePath: `.env.${process.env.NODE_ENV}`,
}), }),
NacosModule, NacosModule,
// RetrievalModule,
LabelModule, LabelModule,
], ],
controllers: [AppController], controllers: [AppController],

View File

@ -1,33 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Study } from './study';
@Entity()
export class ArchiveTask {
@PrimaryGeneratedColumn()
id: number;
@Column()
annotatorId: number;
@Column()
StudyInstanceUID;
@Column()
SeriesInstanceUID: string;
@CreateDateColumn({ type: 'timestamp' })
createTime: Date;
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date;
@OneToMany(() => Study, (study) => study.archiveTask)
studies: Study[];
}

View File

@ -1,32 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Study } from './study';
@Entity()
export class Series {
@PrimaryGeneratedColumn()
id: number;
@Column()
SeriesInstanceUID: string;
@CreateDateColumn({ type: 'timestamp' })
createTime: Date;
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date;
@ManyToOne(() => Study, (study) => study.series)
@JoinColumn({
name: 'StudyInstanceUID',
referencedColumnName: 'StudyInstanceUID',
}) // 自定义连接列
study: Study;
}

View File

@ -1,46 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Series } from './series';
import { ArchiveTask } from './archiveTask';
@Entity()
export class Study {
@PrimaryGeneratedColumn()
id: number;
@Column()
StudyInstanceUID: string;
@Column()
PatientID: string | number;
@Column()
PatientName: string;
@Column()
PatientSex: string;
@Column()
PatientAge: string;
@CreateDateColumn({ type: 'timestamp' })
createTime: Date;
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date;
@OneToMany(() => Series, (series) => series.study)
series: Series[];
@ManyToOne(() => ArchiveTask, (archiveTask) => archiveTask.studies)
@JoinColumn({ name: 'ArchiveTaskId', referencedColumnName: 'id' }) // 自定义连接列
archiveTask: ArchiveTask;
}

View File

@ -1,17 +0,0 @@
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
import { RetrievalService } from './retrieval.service';
@Controller()
export class RetrievalController {
constructor(private readonly retrievalSerivce: RetrievalService) {}
@EventPattern({ cmd: 'dicom.retrieval.archivetask.create' })
async createArchiveTask(payload) {
const { user, study } = payload;
const { id: annotatorId } = user;
return await this.retrievalSerivce.createArchiveTask({
annotatorId,
study,
});
}
}

View File

@ -1,27 +0,0 @@
import { Module } from '@nestjs/common';
import { RetrievalController } from './retrieval.controller';
import { RetrievalService } from './retrieval.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ArchiveTask } from './entity/archiveTask';
import { Series } from './entity/series';
import { Study } from './entity/study';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'dicom',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
timezone: 'Asia/Shanghai', // 这里设置了时区
}),
TypeOrmModule.forFeature([ArchiveTask, Series, Study]),
],
controllers: [RetrievalController],
providers: [RetrievalService],
})
export class RetrievalModule {}

View File

@ -1,16 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ArchiveTask } from './entity/archiveTask';
import { Repository } from 'typeorm';
@Injectable()
export class RetrievalService {
constructor(
@InjectRepository(ArchiveTask)
private readonly archiveTaskRepository: Repository<ArchiveTask>,
) {}
async createArchiveTask({ annotatorId, study }) {
return this.archiveTaskRepository.save({ annotatorId });
}
}