feat: 上传全部影像
This commit is contained in:
parent
09bcfb81b4
commit
267c5dfa43
|
@ -17,8 +17,9 @@ export class UserService {
|
|||
|
||||
async signIn(user: Pick<User, "username" | "password">) {
|
||||
const { code, data, msg } = await this.userRepository.authLogin(user);
|
||||
this.user.signIn(data as User);
|
||||
return { success: code === 0, msg, data: data as User };
|
||||
const success = code === 0;
|
||||
if (success) this.user.signIn(data as User);
|
||||
return { success, msg, data: data as User };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,9 +35,9 @@ export class User {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取角色菜单
|
||||
* 获取用户拥有的角色名数组
|
||||
*/
|
||||
getRolesName() {
|
||||
getRolesNames() {
|
||||
return this.roles?.map((r: { name: string }) => r.name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { Study, parseDcmFiles } from "./util";
|
||||
import { Button } from "antd";
|
||||
import { CloudUploadOutlined } from "@ant-design/icons";
|
||||
import { FolderOutlined } from "@ant-design/icons";
|
||||
|
||||
declare module "react" {
|
||||
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
|
@ -59,11 +59,11 @@ export const useDicomUploader = () => {
|
|||
onChange={(e) => handleFileChange(e)}
|
||||
/>
|
||||
<Button
|
||||
icon={<CloudUploadOutlined />}
|
||||
icon={<FolderOutlined />}
|
||||
type="primary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
上传dicom
|
||||
选择dicom
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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 { Series, Study } from "./DicomUploader/util";
|
||||
import { DicomTable } from "./DicomTable";
|
||||
|
@ -7,7 +17,8 @@ import { Typography } from "antd";
|
|||
import { useState } from "react";
|
||||
import { User } from "@@/domain/User/entities/User";
|
||||
import { limitConcurrency } from "./limitConcurrency";
|
||||
import { useProgress } from "./useProgress";
|
||||
import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons";
|
||||
import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
@ -15,21 +26,9 @@ interface DicomUploadProps {
|
|||
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) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const { UploadInput, fileCalculator, studys, isLoading } = useDicomUploader();
|
||||
const { ProgressModal, showProgress, hideProgress, updatePercent } =
|
||||
useProgress();
|
||||
const { dicomDomainService, userDomainService } = useDomain();
|
||||
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
|
||||
const [selectRows, setSelectedRows] = useState<Study[]>([]);
|
||||
|
@ -38,7 +37,12 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [percent, setPercent] = useState<number>(0);
|
||||
|
||||
/**
|
||||
* 单个序列阅片
|
||||
*/
|
||||
const onUploadFiles = async (study: Study, series: Series) => {
|
||||
const { SeriesInstanceUID, subs } = series;
|
||||
const { StudyInstanceUID } = study;
|
||||
|
@ -50,20 +54,60 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
if (instances.length === subs.length) {
|
||||
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
|
||||
} else {
|
||||
const uploadFunc = (f: File) => () => dicomDomainService.upload2Pacs(f);
|
||||
showProgress();
|
||||
setVisible(true);
|
||||
limitConcurrency(
|
||||
series.subs.map(uploadFunc),
|
||||
series.subs.map((f: File) => () => dicomDomainService.upload2Pacs(f)),
|
||||
10,
|
||||
(completed, total) => {
|
||||
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 = () => {
|
||||
userDomainService.getDmpAnnotators().then((res) => {
|
||||
setIsModalOpen(true);
|
||||
|
@ -71,15 +115,19 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const dcmFileInfo = !!totalFileNum && (
|
||||
<Space>
|
||||
<Text type="secondary">扫描总文件数: {totalFileNum}</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">包含: {dcmFileNum} 个dicom文件</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
|
||||
</Space>
|
||||
);
|
||||
/**
|
||||
* 统计信息
|
||||
*/
|
||||
const DicomFileInfo = () =>
|
||||
!!totalFileNum && (
|
||||
<Space>
|
||||
<Text type="secondary">扫描总文件数: {totalFileNum}</Text>
|
||||
<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>
|
||||
<UploadInput />
|
||||
<Button
|
||||
icon={<CloudUploadOutlined />}
|
||||
disabled={Number(dcmFileNum) === 0}
|
||||
type="primary"
|
||||
onClick={onUploadEntiredDicom}
|
||||
>
|
||||
上传全部
|
||||
</Button>
|
||||
<Button
|
||||
icon={<InboxOutlined />}
|
||||
disabled={selectRows.length === 0}
|
||||
type="primary"
|
||||
onClick={onClickAssign}
|
||||
|
@ -107,7 +164,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
{dcmFileInfo}
|
||||
<DicomFileInfo />
|
||||
<DicomTable
|
||||
studys={studys}
|
||||
loading={isLoading}
|
||||
|
@ -137,7 +194,10 @@ export const DicomUpload = (props: DicomUploadProps) => {
|
|||
}))}
|
||||
/>
|
||||
</Modal>
|
||||
<ProgressModal />
|
||||
<Modal title="上传影像中" open={visible} footer={null} closable={false}>
|
||||
<Progress percent={percent} strokeLinecap="butt" />
|
||||
</Modal>
|
||||
{contextHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,15 +15,7 @@ export async function limitConcurrency(
|
|||
const index = currentIndex;
|
||||
currentIndex += 1;
|
||||
|
||||
if (index >= tasks.length) {
|
||||
if (results.length === tasks.length) {
|
||||
resolve(results);
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (index >= tasks.length) return;
|
||||
|
||||
const task = tasks[index];
|
||||
|
||||
|
@ -34,6 +26,13 @@ export async function limitConcurrency(
|
|||
if (onProgress) {
|
||||
onProgress(completedTasks, tasks.length);
|
||||
}
|
||||
// 当所有任务都完成时触发
|
||||
if (completedTasks === tasks.length) {
|
||||
resolve(results);
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
executeTask();
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
27
apps/dmp/src/modules/Admin/Dicom/Upload/util.ts
Normal file
27
apps/dmp/src/modules/Admin/Dicom/Upload/util.ts
Normal 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,
|
||||
}))
|
||||
);
|
|
@ -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 {
|
||||
BrowserRouter,
|
||||
Route,
|
||||
RouteObject,
|
||||
Routes,
|
||||
useLocation,
|
||||
useRoutes,
|
||||
} from "react-router-dom";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { baseRoutes } from "./baseRoutes";
|
||||
import { roleRoutes } from "./roleRoutes";
|
||||
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 = {
|
||||
title?: string;
|
||||
|
@ -23,14 +16,19 @@ export const RouterElements = observer(() => {
|
|||
const { user } = userDomainService;
|
||||
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
|
||||
|
|
|
@ -1,27 +1,7 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
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()
|
||||
export class AppController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AppService } from './app.service';
|
|||
import { NacosModule } from './nacos/nacos.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RetrievalModule } from './retrieval/retrieval.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -12,6 +13,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
envFilePath: `.env.${process.env.NODE_ENV}`,
|
||||
}),
|
||||
NacosModule,
|
||||
RetrievalModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {}
|
||||
|
|
0
apps/services/dicom/src/retrieval/entity/patient.ts
Normal file
0
apps/services/dicom/src/retrieval/entity/patient.ts
Normal file
0
apps/services/dicom/src/retrieval/entity/series.ts
Normal file
0
apps/services/dicom/src/retrieval/entity/series.ts
Normal file
0
apps/services/dicom/src/retrieval/entity/study.ts
Normal file
0
apps/services/dicom/src/retrieval/entity/study.ts
Normal file
28
apps/services/dicom/src/retrieval/entity/task.ts
Normal file
28
apps/services/dicom/src/retrieval/entity/task.ts
Normal 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;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller('retrieval')
|
||||
export class RetrievalController {}
|
24
apps/services/dicom/src/retrieval/retrieval.module.ts
Normal file
24
apps/services/dicom/src/retrieval/retrieval.module.ts
Normal 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 {}
|
4
apps/services/dicom/src/retrieval/retrieval.service.ts
Normal file
4
apps/services/dicom/src/retrieval/retrieval.service.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class RetrievalService {}
|
|
@ -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 { firstValueFrom } from 'rxjs';
|
||||
|
||||
|
@ -13,4 +13,12 @@ export class UserController {
|
|||
);
|
||||
return { data, code: 0 };
|
||||
}
|
||||
|
||||
@Post("assign")
|
||||
async assign(){
|
||||
const { data } = await firstValueFrom(
|
||||
this.client.send({ cmd: 'dicom.user.find.annotator' }, {}),
|
||||
);
|
||||
return { data, code: 0 };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user