feat: 标签、分类增删改查

This commit is contained in:
mozzie 2023-09-18 14:51:50 +08:00
parent 25ea7c14d9
commit 0bb16c94ae
15 changed files with 546 additions and 67 deletions

View File

@ -0,0 +1,10 @@
export type LabelType = {
name: string;
};
export class Label {
name: string;
constructor(props: LabelType) {
this.name = props.name;
}
}

View File

@ -1,5 +1,11 @@
import { Apis } from "@@/infra/api";
import { LabelCategory } from "./entities/labelCategory";
import {
LabelCategoryUpdateDTO,
LabelCreateDTO,
LabelDeleteDTO,
LabelUpdateDTO,
} from "@@/infra/api/dto";
export class LabelRepository {
async createNewLabelCategory(labelCategory: LabelCategory) {
@ -9,4 +15,20 @@ export class LabelRepository {
async findLabelCategory() {
return await Apis.findLabelCategory();
}
async createLabel(p: LabelCreateDTO) {
return await Apis.createLabel(p);
}
async updateLabel(p: LabelUpdateDTO) {
return await Apis.updateLabel(p);
}
async deleteLabel(p: LabelDeleteDTO) {
return await Apis.deleteLabel(p);
}
async updateCategory(p: LabelCategoryUpdateDTO) {
return await Apis.updateCategory(p);
}
}

View File

@ -1,3 +1,9 @@
import {
LabelCategoryUpdateDTO,
LabelCreateDTO,
LabelDeleteDTO,
LabelUpdateDTO,
} from "@@/infra/api/dto";
import { LabelCategory } from "./entities/labelCategory";
import { LabelRepository } from "./labelRepository";
@ -14,4 +20,24 @@ export class LabelService {
async findLabelCategory() {
return await this.labelRepository.findLabelCategory();
}
async createNewLabel(p: LabelCreateDTO) {
const { code } = await this.labelRepository.createLabel(p);
return { code };
}
async updateLabel(p: LabelUpdateDTO) {
const { code } = await this.labelRepository.updateLabel(p);
return { code };
}
async deleteLabel(p: LabelDeleteDTO) {
const { code } = await this.labelRepository.deleteLabel(p);
return { code };
}
async updateCategory(p: LabelCategoryUpdateDTO) {
const { code } = await this.labelRepository.updateCategory(p);
return { code };
}
}

View File

@ -34,3 +34,22 @@ export type DownloadArchiveDTO = {
export type labelCategoryDTO = {
name: string;
};
export type LabelCreateDTO = {
name: string;
categoryId: number;
};
export type LabelUpdateDTO = {
name: string;
id: number;
};
export type LabelDeleteDTO = {
id: number;
};
export type LabelCategoryUpdateDTO = {
name: string;
id: number;
};

View File

@ -6,10 +6,14 @@ import {
ArchiveTaskCreateDto,
DownloadArchiveDTO,
ExistInPacsDTO,
LabelCreateDTO,
LabelDeleteDTO,
LabelUpdateDTO,
ResponsePacsType,
ResponseType,
labelCategoryDTO,
} from "./dto";
import { Label } from "@@/domain/Label/entities/label";
const PREFIX = "/api/dmp";
const PREFIX_CERT = "/cert";
@ -93,4 +97,28 @@ export const Apis = {
*/
findLabelCategory: (): ResponseType =>
Request.get(PREFIX + "/dicom/label/category/find/all"),
/**
*
*/
createLabel: (p: LabelCreateDTO): ResponseType =>
Request.post(PREFIX + "/dicom/label/create", p),
/**
*
*/
updateLabel: (p: LabelUpdateDTO): ResponseType =>
Request.post(PREFIX + "/dicom/label/update", p),
/**
*
*/
deleteLabel: (p: LabelDeleteDTO): ResponseType =>
Request.post(PREFIX + "/dicom/label/delete", p),
/**
*
*/
updateCategory: (p: labelCategoryDTO): ResponseType =>
Request.post(PREFIX + "/dicom/label/category/update", p),
};

View File

@ -0,0 +1,28 @@
import { ModalType } from "./interface";
type ModalConfigType = {
[key in ModalType]: {
title: string;
placeholder: string;
};
};
export const modalConfig: ModalConfigType = {
createLabel: {
title: "添加新标签",
placeholder: "新标签名称",
},
updateLabel: {
title: "编辑",
placeholder: "新标签名称",
},
updateCategory: {
title: "编辑分类名称",
placeholder: "新标签分类名称",
},
};
export const defaultModalConfig = {
title: "",
placeholder: "",
};

View File

@ -0,0 +1,192 @@
import {
Button,
Col,
Collapse,
CollapseProps,
Form,
Input,
Modal,
Row,
Space,
Tooltip,
theme,
} from "antd";
import {
CategoryItemType,
LabelTreeProps,
LabelType,
ModalType,
} from "./interface";
import { useEffect, useState } from "react";
import { defaultModalConfig, modalConfig } from "./config";
import {
CaretRightOutlined,
CloseOutlined,
EditOutlined,
TagOutlined,
} from "@ant-design/icons";
export const LabelTree = (props: LabelTreeProps) => {
const { token } = theme.useToken();
const [modalType, setModalType] = useState<ModalType | null>(null);
const [form] = Form.useForm<{ name: string }>();
const [activeLabel, setActiveLabel] = useState<LabelType | null>(null);
const [activeCategory, setActiveCategory] = useState<CategoryItemType | null>(
null
);
useEffect(() => {
switch (modalType) {
case "updateLabel":
form.setFieldsValue({ name: activeLabel?.name });
break;
case "updateCategory":
form.setFieldsValue({ name: activeCategory?.name });
break;
default:
break;
}
}, [activeLabel, activeCategory, form, modalType]);
const { title, placeholder } = !modalType
? defaultModalConfig
: modalConfig[modalType];
const handleConfirm = async () => {
try {
await form.validateFields();
const { name } = form.getFieldsValue();
switch (modalType) {
case "createLabel":
activeCategory &&
props.onCreateLabel?.({ name, categoryId: activeCategory.id });
break;
case "updateLabel":
activeLabel && props.onUpdateLabel?.({ name, id: activeLabel.id });
break;
case "updateCategory":
activeCategory &&
props.onUpdateCategory?.({ name, id: activeCategory.id });
break;
default:
break;
}
form.resetFields();
setModalType(null);
} catch (error) {
console.log(error);
}
};
const getItems = (dataList: CategoryItemType[]): CollapseProps["items"] =>
dataList
.map((item) => {
return {
key: item.id,
label: (
<Row>
<Col span={18}>{item.name}</Col>
<Col span={6}>
<Space>
<Tooltip title={`编辑 ${item.name} 分类`}>
<EditOutlined
onClick={(e) => {
e.stopPropagation();
setActiveCategory(item);
setModalType("updateCategory");
}}
/>
</Tooltip>
<Tooltip title={`删除 ${item.name} 分类`}>
<CloseOutlined />
</Tooltip>
</Space>
</Col>
</Row>
),
children: (
<ul className="label-catalog">
{item.labels?.map((label) => (
<li key={label.id}>
<Row>
<Col span={18}>{label.name}</Col>
<Col span={6}>
<Space>
<Tooltip title={`编辑 ${label.name} 标签`}>
<EditOutlined
onClick={(e) => {
e.stopPropagation();
setActiveLabel(label);
setModalType("updateLabel");
}}
/>
</Tooltip>
<Tooltip title={`删除 ${label.name} 标签`}>
<CloseOutlined
onClick={(e) => {
e.stopPropagation();
props.onDeleteLabel?.(label);
}}
/>
</Tooltip>
</Space>
</Col>
</Row>
</li>
))}
<Button
type="dashed"
block
icon={<TagOutlined />}
onClick={() => {
setActiveCategory(item);
setModalType("createLabel");
}}
>
</Button>
</ul>
),
};
})
.map((i) => ({
...i,
headerClass: "label-category-header",
className: "label-category-item",
}));
return (
<div>
<Collapse
bordered={false}
defaultActiveKey={[]}
expandIcon={({ isActive }) => (
<CaretRightOutlined rotate={isActive ? 90 : 0} />
)}
style={{ background: token.colorBgContainer, color: token.colorText }}
items={getItems(props.data)}
/>
<Modal
open={modalType !== null}
title={title}
cancelText="再想想"
okText="确认"
width={350}
onCancel={() => setModalType(null)}
onOk={handleConfirm}
>
<Form form={form}>
<Form.Item
name="name"
rules={[
{ required: true, message: `${title}不能为空` },
{ max: 10, message: "不能超过十个字符" },
]}
>
<Input placeholder={placeholder} />
</Form.Item>
</Form>
</Modal>
</div>
);
};

View File

@ -0,0 +1,47 @@
export type ModalType = "createLabel" | "updateLabel" | "updateCategory";
export type CategoryItemType = {
id: number;
name: string;
labels?: { id: number; name: string }[];
};
export type LabelCategoryType = {
name: string;
id: number;
};
export type LabelType = {
name: string;
id?: number;
categoryId?: number;
};
export interface LabelTreeProps {
children?: JSX.Element;
data: CategoryItemType[];
/**
*
* @param {string} name
* @param {number} id id
*/
onUpdateCategory?: (p: LabelCategoryType) => void;
/**
*
* @param {string} name
* @param {number} categoryId id
*/
onCreateLabel?: (p: LabelType) => void;
/**
*
* @param {string} name
* @param {number} id id
*/
onUpdateLabel?: (p: LabelType) => void;
/**
*
* @param {number} id id
*/
onDeleteLabel?: (p: LabelType) => void;
}

View File

@ -1,84 +1,111 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import "./index.less";
import {
Button,
Collapse,
CollapseProps,
Form,
Input,
Modal,
theme,
Typography,
} from "antd";
import { CaretRightOutlined, PlusOutlined } from "@ant-design/icons";
import { Button, Form, Input, Modal, message } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useDomain } from "@/hook/useDomain";
const { Text } = Typography;
import { LabelTree } from "./LabelTree";
import {
CategoryItemType,
LabelCategoryType,
LabelType,
} from "./LabelTree/interface";
interface LabelProps {
children?: JSX.Element;
}
const content = (
<ul className="label-catalog">
<li></li>
<li className="active">{"层厚>1.0mm"}</li>
</ul>
);
const getItems = (): CollapseProps["items"] =>
[
{
key: "1",
label: "影像质量",
children: content,
},
{
key: "2",
label: "其他",
children: <p>1</p>,
},
].map((i) => ({
...i,
headerClass: "label-category-header",
className: "label-category-item",
}));
export const Label = (props: LabelProps) => {
const { token } = theme.useToken();
const [isModalOpen, setIsModalOpen] = useState(false);
const [createLabelCategoryForm] = Form.useForm<{ name: string }>();
const { labelDomainService } = useDomain();
const [labelList, setLabelList] = useState<CategoryItemType[]>([]);
const [messageApi, contextHolder] = message.useMessage();
const handleOk = async () => {
const createLabelHandler = async () => {
try {
await createLabelCategoryForm.validateFields();
const { name } = createLabelCategoryForm.getFieldsValue();
labelDomainService.createNewLabelCategory({ name });
labelDomainService.createNewLabelCategory({ name }).then((res) => {
const { code } = res;
if (code === 0) {
renderLabelList();
createLabelCategoryForm.resetFields();
setIsModalOpen(false);
}
});
} catch (error) {
console.log(error);
}
};
useEffect(() => {
labelDomainService.findLabelCategory();
const renderLabelList = useCallback(async () => {
const { code, data } = await labelDomainService.findLabelCategory();
if (code === 0) setLabelList(data as CategoryItemType[]);
}, [labelDomainService]);
useEffect(() => {
renderLabelList();
}, [renderLabelList]);
const onCreateLabelHandler = ({ name, categoryId }: LabelType) => {
labelDomainService
.createNewLabel({ name, categoryId: categoryId! })
.then((res) => {
const { code } = res;
if (code === 0) {
renderLabelList();
messageApi.success(`标签 ${name} 添加成功`);
}
});
};
const onUpdateLabelHandler = ({ name, id }: LabelType) => {
labelDomainService.updateLabel({ name, id: id! }).then((res) => {
const { code } = res;
if (code === 0) {
renderLabelList();
messageApi.success(`标签 ${name} 修改成功`);
}
});
};
const onDeleteLabelHandler = ({ id, name }: LabelType) => {
labelDomainService.deleteLabel({ id: id! }).then((res) => {
const { code } = res;
if (code === 0) {
renderLabelList();
messageApi.success(`标签 ${name} 修改成功`);
}
});
};
const onUpdateCategoryHandler = ({ name, id }: LabelCategoryType) => {
labelDomainService.updateCategory({ name, id }).then((res) => {
const { code } = res;
if (code === 0) {
renderLabelList();
messageApi.success(`${name} 分类修改成功`);
}
});
};
return (
<div className="label-container">
{contextHolder}
<aside>
<Button
block
type="dashed"
type="primary"
icon={<PlusOutlined />}
style={{ marginBottom: 10 }}
onClick={() => setIsModalOpen(true)}
>
</Button>
<Modal
title="新增标签分类"
title="创建标签分类"
open={isModalOpen}
onOk={handleOk}
onOk={createLabelHandler}
width={350}
okText="确认"
cancelText="再想想"
@ -96,14 +123,12 @@ export const Label = (props: LabelProps) => {
</Form.Item>
</Form>
</Modal>
<Collapse
bordered={false}
defaultActiveKey={[]}
expandIcon={({ isActive }) => (
<CaretRightOutlined rotate={isActive ? 90 : 0} />
)}
style={{ background: token.colorBgContainer, color: token.colorText }}
items={getItems()}
<LabelTree
data={labelList}
onCreateLabel={onCreateLabelHandler}
onUpdateLabel={onUpdateLabelHandler}
onDeleteLabel={onDeleteLabelHandler}
onUpdateCategory={onUpdateCategoryHandler}
/>
</aside>
<main></main>

View File

@ -15,8 +15,9 @@ const errorMapping = {
@Catch(QueryFailedError)
export class TypeOrmExceptionFilter implements ExceptionFilter {
catch(exception: QueryFailedError, host: ArgumentsHost) {
console.log('TypeOrmExceptionFilter 过滤器');
const message = errorMapping?.[exception.driverError] ?? 'sql操作失败。';
console.log('TypeOrmExceptionFilter 过滤器:', exception.driverError);
const message =
errorMapping?.[exception.driverError.code] ?? 'sql操作失败。';
return { statusCode, message };
}
}

View File

@ -2,9 +2,11 @@ import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Label } from './label.entity';
@Entity()
export class LabelCategory {
@ -19,4 +21,7 @@ export class LabelCategory {
@UpdateDateColumn({ type: 'timestamp' })
updateAt: Date;
@OneToMany(() => Label, (label) => label.category)
labels: Label[];
}

View File

@ -3,9 +3,11 @@ import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { LabelCategory } from './category.entity';
@Entity()
export class Label {
@ -27,4 +29,7 @@ export class Label {
@UpdateDateColumn({ type: 'timestamp' })
updateAt: Date;
@ManyToOne(() => LabelCategory, (category) => category.labels)
category: LabelCategory;
}

View File

@ -1,5 +1,5 @@
import { Controller } from '@nestjs/common';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
import { MessagePattern } from '@nestjs/microservices';
import { LabelService } from './label.service';
@Controller()
@ -12,13 +12,32 @@ export class LabelController {
return { statusCode: 200, data: inserted };
}
@EventPattern({ cmd: 'dicom.label.create' })
@MessagePattern('dicom.label.create')
async createLabel(payload) {
return this.labelService.createLabel(payload);
const data = await this.labelService.createLabel(payload);
return { statusCode: 200, data };
}
@MessagePattern('dicom.label.update')
async updateLabel(payload) {
const data = await this.labelService.updateLabel(payload);
return { statusCode: 200, data };
}
@MessagePattern('dicom.label.category.find.all')
async findAllLabelCategory() {
return await this.labelService.findAllLabelCategory();
}
@MessagePattern('dicom.label.delete')
async deleteLabel(payload) {
const { affected } = await this.labelService.deleteLabel(payload);
return { statusCode: affected > 0 ? 200 : -1 };
}
@MessagePattern('dicom.label.category.update')
async updateLabelCategory(payload) {
const { affected } = await this.labelService.updateLabelCategory(payload);
return { statusCode: affected > 0 ? 200 : -1 };
}
}

View File

@ -17,15 +17,31 @@ export class LabelService {
return await this.labelCategoryRepository.save({ name });
}
async createLabel(payload) {
console.log(payload);
return await this.labelRepository.save({
name: '钙化',
description: '钙化的关键词',
async createLabel({ categoryId: id, name }) {
const category = await this.labelCategoryRepository.findOne({
where: { id },
});
const label = new Label();
label.name = name;
label.category = category;
return await this.labelRepository.save(label);
}
async updateLabel({ id, name }) {
const label = await this.labelRepository.findOne({ where: { id } });
label.name = name;
return await this.labelRepository.save(label);
}
async findAllLabelCategory() {
return await this.labelCategoryRepository.find();
return await this.labelCategoryRepository.find({ relations: ['labels'] });
}
async deleteLabel({ id }) {
return await this.labelRepository.delete({ id });
}
async updateLabelCategory({ name, id }) {
return await this.labelCategoryRepository.update(id, { name });
}
}

View File

@ -32,7 +32,7 @@ export class DicomController {
const { statusCode, message, data } = await firstValueFrom(
this.client.send('dicom.label.category.create', { name }),
);
return { statusCode, message, data };
return { code: statusCode === 200 ? 0 : 1, message, data };
}
@Get('label/category/find/all')
@ -42,4 +42,40 @@ export class DicomController {
);
return { code: 0, data };
}
@Post('label/create')
async createLabel(@Body() body) {
const { name, categoryId } = body;
const { statusCode, message, data } = await firstValueFrom(
this.client.send('dicom.label.create', { name, categoryId }),
);
return { code: statusCode === 200 ? 0 : 1, message, data };
}
@Post('label/update')
async updateLabel(@Body() body) {
const { name, id } = body;
const { statusCode, message, data } = await firstValueFrom(
this.client.send('dicom.label.update', { name, id }),
);
return { code: statusCode === 200 ? 0 : 1, message, data };
}
@Post('label/delete')
async deleteLabel(@Body() body) {
const { id } = body;
const { statusCode, message, data } = await firstValueFrom(
this.client.send('dicom.label.delete', { id }),
);
return { code: statusCode === 200 ? 0 : 1, message, data };
}
@Post('label/category/update')
async updateLabelCategory(@Body() body) {
const { name, id } = body;
const { statusCode, message, data } = await firstValueFrom(
this.client.send('dicom.label.category.update', { name, id }),
);
return { code: statusCode === 200 ? 0 : 1, message, data };
}
}