feat: 批量上传pacs
This commit is contained in:
parent
d60469dae8
commit
6f62e35ff3
|
@ -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">) {
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 });
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user