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 { 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">) {
|
||||||
|
|
|
@ -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>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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