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 { ArchiveTaskCreateDto } from "@@/infra/api/dto";
export class UserRepository {
async authLogin(user: Pick<User, "username" | "password">) {

View File

@ -1,5 +1,5 @@
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 { Button, Space, Table, Tooltip } from "antd";
interface DicomTableProps {
@ -26,14 +26,13 @@ export const DicomTable = (props: DicomTableProps) => {
dataIndex: "operation",
render: (_: any, recordSeries: Series) => (
<Space>
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
onClick={() => props.onUploadFiles?.(record, recordSeries)}
>
</Button>
<Tooltip title="在线阅片">
<Button
type="text"
icon={<DesktopOutlined />}
onClick={() => props.onUploadFiles?.(record, recordSeries)}
/>
</Tooltip>
</Space>
),
},

View File

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

View File

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

View File

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

View File

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