feat: 上传全部影像

This commit is contained in:
mozzie 2023-08-30 16:03:09 +08:00
parent 09bcfb81b4
commit 267c5dfa43
19 changed files with 218 additions and 117 deletions

View File

@ -17,8 +17,9 @@ export class UserService {
async signIn(user: Pick<User, "username" | "password">) { async signIn(user: Pick<User, "username" | "password">) {
const { code, data, msg } = await this.userRepository.authLogin(user); const { code, data, msg } = await this.userRepository.authLogin(user);
this.user.signIn(data as User); const success = code === 0;
return { success: code === 0, msg, data: data as User }; if (success) this.user.signIn(data as User);
return { success, msg, data: data as User };
} }
/** /**

View File

@ -35,9 +35,9 @@ export class User {
} }
/** /**
* *
*/ */
getRolesName() { getRolesNames() {
return this.roles?.map((r: { name: string }) => r.name); return this.roles?.map((r: { name: string }) => r.name);
} }
} }

View File

@ -1,7 +1,7 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Study, parseDcmFiles } from "./util"; import { Study, parseDcmFiles } from "./util";
import { Button } from "antd"; import { Button } from "antd";
import { CloudUploadOutlined } from "@ant-design/icons"; import { FolderOutlined } from "@ant-design/icons";
declare module "react" { declare module "react" {
interface InputHTMLAttributes<T> extends HTMLAttributes<T> { interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
@ -59,11 +59,11 @@ export const useDicomUploader = () => {
onChange={(e) => handleFileChange(e)} onChange={(e) => handleFileChange(e)}
/> />
<Button <Button
icon={<CloudUploadOutlined />} icon={<FolderOutlined />}
type="primary" type="primary"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
> >
dicom dicom
</Button> </Button>
</> </>
); );

View File

@ -1,4 +1,14 @@
import { Button, Col, Divider, Modal, Row, Select, Space, Spin } from "antd"; import {
Button,
Col,
Divider,
Modal,
Progress,
Row,
Select,
Space,
message,
} from "antd";
import { useDicomUploader } from "./DicomUploader"; import { useDicomUploader } from "./DicomUploader";
import { Series, Study } from "./DicomUploader/util"; import { Series, Study } from "./DicomUploader/util";
import { DicomTable } from "./DicomTable"; import { DicomTable } from "./DicomTable";
@ -7,7 +17,8 @@ import { Typography } from "antd";
import { useState } from "react"; import { useState } from "react";
import { User } from "@@/domain/User/entities/User"; import { User } from "@@/domain/User/entities/User";
import { limitConcurrency } from "./limitConcurrency"; import { limitConcurrency } from "./limitConcurrency";
import { useProgress } from "./useProgress"; import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons";
import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util";
const { Text } = Typography; const { Text } = Typography;
@ -15,21 +26,9 @@ interface DicomUploadProps {
children?: JSX.Element; children?: JSX.Element;
} }
/**
* ohif阅片
*/
const openOHIFViewer = (
StudyInstanceUID: string,
SeriesInstanceUID: string
) => {
const target = `http://localhost:3000/viewer/${StudyInstanceUID}?SeriesInstanceUID=${SeriesInstanceUID}`;
window.open(target, "_blank");
};
export const DicomUpload = (props: DicomUploadProps) => { export const DicomUpload = (props: DicomUploadProps) => {
const [messageApi, contextHolder] = message.useMessage();
const { UploadInput, fileCalculator, studys, isLoading } = useDicomUploader(); const { UploadInput, fileCalculator, studys, isLoading } = useDicomUploader();
const { ProgressModal, showProgress, hideProgress, updatePercent } =
useProgress();
const { dicomDomainService, userDomainService } = useDomain(); const { dicomDomainService, userDomainService } = useDomain();
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator; const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
const [selectRows, setSelectedRows] = useState<Study[]>([]); const [selectRows, setSelectedRows] = useState<Study[]>([]);
@ -38,7 +37,12 @@ export const DicomUpload = (props: DicomUploadProps) => {
const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>( const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>(
undefined undefined
); );
const [visible, setVisible] = useState<boolean>(false);
const [percent, setPercent] = useState<number>(0);
/**
*
*/
const onUploadFiles = async (study: Study, series: Series) => { const onUploadFiles = async (study: Study, series: Series) => {
const { SeriesInstanceUID, subs } = series; const { SeriesInstanceUID, subs } = series;
const { StudyInstanceUID } = study; const { StudyInstanceUID } = study;
@ -50,20 +54,60 @@ export const DicomUpload = (props: DicomUploadProps) => {
if (instances.length === subs.length) { if (instances.length === subs.length) {
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID); openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
} else { } else {
const uploadFunc = (f: File) => () => dicomDomainService.upload2Pacs(f); setVisible(true);
showProgress();
limitConcurrency( limitConcurrency(
series.subs.map(uploadFunc), 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.`);
updatePercent(Math.floor((completed / total) * 100)); setPercent(Math.floor((completed / total) * 100));
}, },
() => hideProgress() () => {
setVisible(false);
setPercent(0);
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
}
); );
} }
}; };
/**
*
*/
const onUploadEntiredDicom = () => {
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);
})
) as Promise<FlatStudyItem | false>[];
// 上传到pacs
Promise.all(checkExistPromises).then((result) => {
const files = result.map((i) => (!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.`);
setPercent(Math.floor((completed / total) * 100));
},
() => {
setVisible(false);
setPercent(0);
}
);
});
};
const onClickAssign = () => { const onClickAssign = () => {
userDomainService.getDmpAnnotators().then((res) => { userDomainService.getDmpAnnotators().then((res) => {
setIsModalOpen(true); setIsModalOpen(true);
@ -71,15 +115,19 @@ export const DicomUpload = (props: DicomUploadProps) => {
}); });
}; };
const dcmFileInfo = !!totalFileNum && ( /**
<Space> *
<Text type="secondary">: {totalFileNum}</Text> */
<Divider type="vertical" /> const DicomFileInfo = () =>
<Text type="secondary">: {dcmFileNum} dicom文件</Text> !!totalFileNum && (
<Divider type="vertical" /> <Space>
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text> <Text type="secondary">: {totalFileNum}</Text>
</Space> <Divider type="vertical" />
); <Text type="secondary">: {dcmFileNum} dicom文件</Text>
<Divider type="vertical" />
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
</Space>
);
/** /**
* *
@ -98,6 +146,15 @@ export const DicomUpload = (props: DicomUploadProps) => {
<Space> <Space>
<UploadInput /> <UploadInput />
<Button <Button
icon={<CloudUploadOutlined />}
disabled={Number(dcmFileNum) === 0}
type="primary"
onClick={onUploadEntiredDicom}
>
</Button>
<Button
icon={<InboxOutlined />}
disabled={selectRows.length === 0} disabled={selectRows.length === 0}
type="primary" type="primary"
onClick={onClickAssign} onClick={onClickAssign}
@ -107,7 +164,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
</Space> </Space>
</Col> </Col>
</Row> </Row>
{dcmFileInfo} <DicomFileInfo />
<DicomTable <DicomTable
studys={studys} studys={studys}
loading={isLoading} loading={isLoading}
@ -137,7 +194,10 @@ export const DicomUpload = (props: DicomUploadProps) => {
}))} }))}
/> />
</Modal> </Modal>
<ProgressModal /> <Modal title="上传影像中" open={visible} footer={null} closable={false}>
<Progress percent={percent} strokeLinecap="butt" />
</Modal>
{contextHolder}
</div> </div>
); );
}; };

View File

@ -15,15 +15,7 @@ export async function limitConcurrency(
const index = currentIndex; const index = currentIndex;
currentIndex += 1; currentIndex += 1;
if (index >= tasks.length) { if (index >= tasks.length) return;
if (results.length === tasks.length) {
resolve(results);
if (onComplete) {
onComplete();
}
}
return;
}
const task = tasks[index]; const task = tasks[index];
@ -34,6 +26,13 @@ export async function limitConcurrency(
if (onProgress) { if (onProgress) {
onProgress(completedTasks, tasks.length); onProgress(completedTasks, tasks.length);
} }
// 当所有任务都完成时触发
if (completedTasks === tasks.length) {
resolve(results);
if (onComplete) {
onComplete();
}
}
executeTask(); executeTask();
}) })
.catch((err) => { .catch((err) => {

View File

@ -1,32 +0,0 @@
import { useState, useCallback } from "react";
import { Modal, Progress } from "antd";
export const useProgress = () => {
const [visible, setVisible] = useState(false);
const [percent, setPercent] = useState(0);
const showProgress = useCallback(() => {
setVisible(true);
}, []);
const hideProgress = useCallback(() => {
setVisible(false);
}, []);
const updatePercent = useCallback((newPercent: number) => {
setPercent(newPercent);
}, []);
const ProgressModal = () => (
<Modal open={visible} footer={null} closable={false}>
<Progress percent={percent} />
</Modal>
);
return {
ProgressModal,
showProgress,
hideProgress,
updatePercent,
};
};

View File

@ -0,0 +1,27 @@
import { Study } from "./DicomUploader/util";
/**
* ohif阅片
*/
export const openOHIFViewer = (
StudyInstanceUID: string,
SeriesInstanceUID: string
) => {
const target = `http://localhost:3000/viewer/${StudyInstanceUID}?SeriesInstanceUID=${SeriesInstanceUID}`;
window.open(target, "_blank");
};
export type FlatStudyItem = {
StudyInstanceUID: string;
SeriesInstanceUID: string;
Files: File[];
};
export const flatternStudies = (studies: Study[]): FlatStudyItem[] =>
studies.flatMap((study) =>
study.subs.map((series) => ({
StudyInstanceUID: study.StudyInstanceUID,
SeriesInstanceUID: series.SeriesInstanceUID,
Files: series.subs,
}))
);

View File

@ -1,18 +1,11 @@
import { Route, RouteObject, Routes, useLocation } from "react-router-dom";
import { ROLE_NAME, defaultDocumentTitle } from "@/constant";
import { pathToRegexp } from "path-to-regexp";
import { useDomain } from "@/hook/useDomain"; import { useDomain } from "@/hook/useDomain";
import { import { observer } from "mobx-react-lite";
BrowserRouter,
Route,
RouteObject,
Routes,
useLocation,
useRoutes,
} from "react-router-dom";
import { baseRoutes } from "./baseRoutes"; import { baseRoutes } from "./baseRoutes";
import { roleRoutes } from "./roleRoutes"; import { roleRoutes } from "./roleRoutes";
import { RouteGuard } from "./Guard"; import { RouteGuard } from "./Guard";
import { observer } from "mobx-react-lite";
import { ROLE_NAME, defaultDocumentTitle } from "@/constant";
import { pathToRegexp } from "path-to-regexp";
export type ExpandRouteProps = { export type ExpandRouteProps = {
title?: string; title?: string;
@ -23,14 +16,19 @@ export const RouterElements = observer(() => {
const { user } = userDomainService; const { user } = userDomainService;
const location = useLocation(); const location = useLocation();
const roleNames = user.roles?.map( /**
(r: { name: string }) => r.name *
) as ROLE_NAME[]; */
const userRoleNames = user.getRolesNames() as ROLE_NAME[];
/** /**
* *
*/ */
const currentRoutes = roleNames?.map((name) => roleRoutes[name]).flat() ?? []; const currentRoutes =
userRoleNames
?.map((n) => roleRoutes[n])
.filter(Boolean)
.flat() ?? [];
/** /**
* document.title * document.title

View File

@ -1,27 +1,7 @@
import { Controller } from '@nestjs/common'; import { Controller } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { EventPattern } from '@nestjs/microservices';
import * as dayjs from 'dayjs';
import { SymmetricCrypto } from '@tavi/util';
interface UserSignLoggerDto {
platform: string;
username: string;
finger: string;
finger2: string;
isLegal: boolean;
}
@Controller() @Controller()
export class AppController { export class AppController {
constructor(private readonly appService: AppService) {} constructor(private readonly appService: AppService) {}
@EventPattern({ cmd: 'logger.user.signIn' })
async userSignIn(payload: UserSignLoggerDto) {
const dateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
const { finger2, ...rest } = payload;
const browserInfo = new SymmetricCrypto().decrypt(finger2);
console.log({ ...rest, dateTime, browserInfo });
return 1;
}
} }

View File

@ -4,6 +4,7 @@ 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 { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { RetrievalModule } from './retrieval/retrieval.module';
@Module({ @Module({
imports: [ imports: [
@ -12,6 +13,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
envFilePath: `.env.${process.env.NODE_ENV}`, envFilePath: `.env.${process.env.NODE_ENV}`,
}), }),
NacosModule, NacosModule,
RetrievalModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@ -1,6 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable() @Injectable()
export class AppService {} export class AppService {}

View File

@ -0,0 +1,28 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
annotatorId: number;
@Column()
StudyInstanceUID;
@Column()
SeriesInstanceUID: string;
@CreateDateColumn({ type: 'timestamp' })
createTime: Date;
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date;
}

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('retrieval')
export class RetrievalController {}

View File

@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { RetrievalController } from './retrieval.controller';
import { RetrievalService } from './retrieval.service';
import { TypeOrmModule } from '@nestjs/typeorm';
@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([]),
],
controllers: [RetrievalController],
providers: [RetrievalService],
})
export class RetrievalModule {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RetrievalService {}

View File

@ -1,4 +1,4 @@
import { Controller, Get, Inject } from '@nestjs/common'; import { Controller, Get, Inject, Post } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices'; import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
@ -13,4 +13,12 @@ export class UserController {
); );
return { data, code: 0 }; return { data, code: 0 };
} }
@Post("assign")
async assign(){
const { data } = await firstValueFrom(
this.client.send({ cmd: 'dicom.user.find.annotator' }, {}),
);
return { data, code: 0 };
}
} }