first commit
This commit is contained in:
commit
9578a29130
26
.changeset/README.md
Normal file
26
.changeset/README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
|
||||
|
||||
## config.json
|
||||
|
||||
- commit:类型为布尔值,默认值为false。当将此字段配置为true时,在执行change和bump命令时,将自动执行提交代码操作。
|
||||
- access:类型为restricted | public,默认值为restricted。用于配置当前包的发布形式,如果配置为restricted,则作为私有包发布,如果为public,则发布公共范围包。
|
||||
- baseBranch:类型为字符串,默认值为main。仓库主分支。该配置用于计算当前分支的变更包并进行分类。
|
||||
- ignore:类型为字符串数组,默认值为空数组。用于声明执行bump命令时忽略的包,与bump命令的–ignore参数用法一致,注意两者不能同时使用。
|
||||
- fixed:类型为字符串数组数组,默认值为空数组。用于在monorepo中对包进行分组,相同分组中的包版本号将进行绑定,每次执行bump命令时,同一分组中的包只要有一个升级版本号,其他会一起升级。支持使用正则匹配包名称。
|
||||
- linked:类型为字符串数组数组,默认值为空数组。与fixed类似,也是对monorepo中对包进行分组,但是每次执行bump命令时,只有和changeset声明的变更相关的包才会升级版本号,同一分组的变更包的版本号将保持一致。支持使用正则匹配包名称。
|
||||
- updateInternalDependencies:类型为patch | minor,默认值为patch。用于声明更新内部依赖的版本号规则。当执行bump命令升级版本号时,默认会自动更新仓库中使用该包的依赖声明。
|
||||
- changelog:类型为布尔值 | 字符串 | [字符串, unknow],默认值为@changesets/cli/changelog。生成Changelog规则。
|
||||
|
||||
## bump
|
||||
|
||||
MAJOR(主要)版本:当你做出不兼容的 API 更改时,需要增加`主版本`号。
|
||||
MINOR(次要)版本:当你以向后兼容的方式引入新功能时,需要增加`次版本`号。
|
||||
PATCH(补丁)版本:当你进行向后兼容的错误修复时,需要增加补丁版本号。
|
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
5
.changeset/tame-phones-dress.md
Normal file
5
.changeset/tame-phones-dress.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@tavi/i18n": patch
|
||||
---
|
||||
|
||||
publishi test
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
**/node_modules
|
||||
**/dist
|
||||
**/.DS_Store
|
||||
**/pg_data
|
||||
**/orthanc_db
|
2
.npmrc
Normal file
2
.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
@tavi:registry=https://npm.saint-medical.com/
|
||||
//npm.saint-medical.com/:_authToken=h/wqiL/6HeZCKtXpd72jTg==
|
81
README.md
Normal file
81
README.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
# 基础服务
|
||||
|
||||
|
||||
## NATS
|
||||
|
||||
```bash
|
||||
docker run -d --name nats -p 4222:4222 -p 6222:6222 -p 8222:8222 nats -m 5000
|
||||
```
|
||||
|
||||
# mysql
|
||||
|
||||
```bash
|
||||
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
|
||||
```
|
||||
|
||||
## nacos
|
||||
|
||||
`http://<your-docker-host-ip>:8848/nacos` 来访问Nacos控制台。默认的用户名和密码都是 `nacos`
|
||||
|
||||
```bash
|
||||
# 单节点模式
|
||||
docker run -d -p 8848:8848 -e MODE=standalone nacos/nacos-server --name nacos
|
||||
```
|
||||
|
||||
## redis
|
||||
|
||||
```
|
||||
docker run --name redis -d -p 6379:6379 -e TZ=Asia/Shanghai redis:alpine --requirepass redis
|
||||
```
|
||||
|
||||
## 普罗米修斯
|
||||
|
||||
```
|
||||
docker run -p 9090:9090 -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
|
||||
```
|
||||
|
||||
|
||||
## nestjs 相关
|
||||
|
||||
|
||||
providers 是一个数组,用于列出此模块中提供的服务。这些服务可以被该模块中的其他部分(例如控制器和其他服务)通过依赖注入(Dependency Injection)进行使用。一旦你将一个类列为模块的provider,NestJS就会负责在需要时创建和销毁这个类的实例。
|
||||
|
||||
imports 是一个数组,用于列出此模块依赖的其他模块。通过这种方式,一个模块可以访问另一个模块中的providers。简单来说,如果你想要在一个模块的服务中使用另一个模块的服务,你就需要将那个模块添加到imports数组中。
|
||||
|
||||
exports 是一个数组,用于列出此模块想要公开的providers,使得其他模块可以访问这些providers。如果你想让一个模块的服务可以被其他模块使用,你就需要将那个服务添加到exports数组中。
|
||||
|
||||
```ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CatsController } from './cats.controller';
|
||||
import { CatsService } from './cats.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule], // 此模块依赖DatabaseModule
|
||||
controllers: [CatsController],
|
||||
providers: [CatsService], // 在此模块中定义CatsService
|
||||
exports: [CatsService], // 允许其他模块访问CatsService
|
||||
})
|
||||
export class CatsModule {}
|
||||
```
|
||||
|
||||
# ohifv3
|
||||
|
||||
```bash
|
||||
# docker run -d -p 3000:80/tcp --name LatestReleasedOHIF ohif/app:latest
|
||||
docker run -d -p 3000:80/tcp -v /Users/mozzie/Desktop/tavi-universe/ohifv3/app-config.js:/usr/share/nginx/html/app-config.js -v /Users/mozzie/Desktop/tavi-universe/ohifv3/conf.d:/etc/nginx/conf.d --name ohifv3 ohif/app:latest
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Minio
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always \
|
||||
-p 19000:9000 -p 19001:9001 \
|
||||
--name minio1 \
|
||||
-v /Users/mozzie/minio:/data \
|
||||
-e "MINIO_ROOT_USER=minio" \
|
||||
-e "MINIO_ROOT_PASSWORD=12345678" \
|
||||
minio/minio server /data --console-address ":9001"
|
||||
```
|
25
apps/aorta/CHANGELOG.md
Normal file
25
apps/aorta/CHANGELOG.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# @tavi/aorta
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- test3
|
||||
- Updated dependencies
|
||||
- @tavi/i18n@1.5.0
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- update test
|
||||
- Updated dependencies
|
||||
- @tavi/i18n@1.4.0
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies
|
||||
- @tavi/i18n@1.1.0
|
29
apps/aorta/babel.config.js
Normal file
29
apps/aorta/babel.config.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const isDEV = process.env.NODE_ENV === "development"; // 是否是开发模式
|
||||
|
||||
module.exports = {
|
||||
// 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
|
||||
// "targets": {
|
||||
// "chrome": 35,
|
||||
// "ie": 9
|
||||
// },
|
||||
targets: { browsers: ["> 1%", "last 2 versions", "not ie <= 8"] },
|
||||
useBuiltIns: "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
|
||||
corejs: 3, // 配置使用core-js使用的版本
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
// 如果您使用的是 Babel 和 React 17,您可能需要将 "runtime": "automatic" 添加到配置中。
|
||||
// 否则可能会出现错误:Uncaught ReferenceError: React is not defined
|
||||
["@babel/preset-react", { runtime: "automatic" }],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [
|
||||
["@babel/plugin-proposal-decorators", { legacy: true }], //装饰器语法
|
||||
isDEV && require.resolve("react-refresh/babel"), // 如果是开发模式,就启动react热更新插件
|
||||
].filter(Boolean), // 过滤空值
|
||||
};
|
11
apps/aorta/core/application/readme.md
Normal file
11
apps/aorta/core/application/readme.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
应用层(application):这一层负责协调领域层和基础设施层,实现具体的用例逻辑。
|
||||
|
||||
协调领域服务:如果有一种业务用例需要多个领域对象或领域服务协作完成,UserApplicationService就可以负责调度它们,然后完成业务逻辑。比如,在创建一个新用户的时候,可能需要检查用户名是否已经存在,然后再创建新的用户实体,这就需要UserApplicationService协调不同的领域服务。
|
||||
|
||||
事务控制:UserApplicationService也可能负责控制数据库事务。虽然在前端的场景中,事务控制可能并不常见,但在后端服务中这是非常常见的。比如,创建一个新用户可能涉及到在几个数据库表中插入数据,这就需要在一个数据库事务中完成。
|
||||
|
||||
安全和授权:UserApplicationService可能需要检查当前用户是否有权限执行某项操作。比如,在更新用户信息的时候,可能需要检查当前用户是否有权限更新这个用户的信息。
|
||||
|
||||
适配领域层与接口层:UserApplicationService也可能负责转换数据格式,以便领域层和接口层之间的数据交换。例如,将领域实体转换为DTO(数据传输对象),或者将来自接口层的请求数据转换为领域服务可以处理的格式。
|
||||
|
||||
在设计UserApplicationService时,需要注意的是,业务逻辑应该尽量放在领域层处理,应用层更多地是做协调和编排的工作,而不是包含业务逻辑。这样可以保证业务逻辑的集中和一致性,也使得业务逻辑更易于测试和重用。
|
3
apps/aorta/core/domain/Base/emitter/index.ts
Normal file
3
apps/aorta/core/domain/Base/emitter/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import mitt from "mitt";
|
||||
|
||||
export const emitter = mitt();
|
5
apps/aorta/core/domain/Tracker/TrackerRepository.ts
Normal file
5
apps/aorta/core/domain/Tracker/TrackerRepository.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Apis } from "@@/infra/api";
|
||||
|
||||
export class TrackerRepository {
|
||||
async report(msg: string) {}
|
||||
}
|
19
apps/aorta/core/domain/User/UserRepository.ts
Normal file
19
apps/aorta/core/domain/User/UserRepository.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { UserInfo } from "./entities/UserInfo";
|
||||
import { Apis } from "@@/infra/api";
|
||||
export class UserRepository {
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
async authLogin(user: UserInfo) {
|
||||
const res = await Apis.signIn(user);
|
||||
return res;
|
||||
}
|
||||
|
||||
async userAuth() {
|
||||
return (await Apis.userAuth()) as {
|
||||
data: { tokenValid: boolean };
|
||||
msg: string;
|
||||
code: number;
|
||||
};
|
||||
}
|
||||
}
|
49
apps/aorta/core/domain/User/UserService.ts
Normal file
49
apps/aorta/core/domain/User/UserService.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { UserLoginForm } from "./entities/UserLoginForm";
|
||||
import { FormFields } from "@/modules/Login/LoginForm";
|
||||
import { UserRepository } from "./UserRepository";
|
||||
import { User } from "./entities/User";
|
||||
|
||||
export class UserService {
|
||||
user: User;
|
||||
loginForm: UserLoginForm;
|
||||
|
||||
constructor(private userRepository: UserRepository) {
|
||||
this.user = new User();
|
||||
this.loginForm = new UserLoginForm();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* update User领域models中的loginForm对象
|
||||
*/
|
||||
updateLoginForm(values: FormFields) {
|
||||
this.loginForm = values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录事件
|
||||
*/
|
||||
async handleLogin(values: FormFields) {
|
||||
const { username, password } = values;
|
||||
const { code, msg, data } = await this.userRepository.authLogin({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
return { success: code === "ok", code, msg };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
async userAuth() {
|
||||
const { code, data, msg } = await this.userRepository.userAuth();
|
||||
if (code === 0 && data?.tokenValid) {
|
||||
this.user.signIn();
|
||||
return { success: true, data };
|
||||
} else {
|
||||
this.user.signOut();
|
||||
return { success: false, msg };
|
||||
}
|
||||
}
|
||||
}
|
25
apps/aorta/core/domain/User/entities/User.ts
Normal file
25
apps/aorta/core/domain/User/entities/User.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { UserInfo } from "./UserInfo";
|
||||
|
||||
type UserProps = {
|
||||
userInfo: UserInfo;
|
||||
isLoggedIn: boolean;
|
||||
};
|
||||
|
||||
export class User {
|
||||
userInfo: UserInfo;
|
||||
isLoggedIn: boolean;
|
||||
constructor(p?: UserProps) {
|
||||
this.userInfo = p?.userInfo ?? {};
|
||||
this.isLoggedIn = false;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
signIn() {
|
||||
this.isLoggedIn = true;
|
||||
}
|
||||
|
||||
signOut() {
|
||||
this.isLoggedIn = false;
|
||||
}
|
||||
}
|
30
apps/aorta/core/domain/User/entities/UserInfo.ts
Normal file
30
apps/aorta/core/domain/User/entities/UserInfo.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
type UserInfoProps = {
|
||||
id?: number;
|
||||
username?: string;
|
||||
/**
|
||||
* 是否登录
|
||||
*/
|
||||
isLoggedIn?: boolean;
|
||||
phoneNumber?: string | number;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createTime?: number | string;
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
department?: string;
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
contactPerson?: string;
|
||||
};
|
||||
|
||||
export class UserInfo {
|
||||
constructor(userInfo: UserInfoProps) {
|
||||
Object.assign(this, userInfo);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
}
|
19
apps/aorta/core/domain/User/entities/UserLoginForm.ts
Normal file
19
apps/aorta/core/domain/User/entities/UserLoginForm.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export type UserLoginFormType = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
phoneNumber?: string | number;
|
||||
verifyCode?: string | number;
|
||||
};
|
||||
|
||||
export class UserLoginForm {
|
||||
username?: string = "";
|
||||
password?: string = "";
|
||||
phoneNumber?: string | number = "";
|
||||
verifyCode?: string | number = "";
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
}
|
16
apps/aorta/core/domain/User/events/UserLoggedIn.ts
Normal file
16
apps/aorta/core/domain/User/events/UserLoggedIn.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
type UserLoggedInProps = {
|
||||
userId: number;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export class UserLoggedIn {
|
||||
userId: number;
|
||||
timestamp: number;
|
||||
|
||||
constructor({ userId, timestamp = Date.now() }: UserLoggedInProps) {
|
||||
this.userId = userId;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
132
apps/aorta/core/domain/readme.md
Normal file
132
apps/aorta/core/domain/readme.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
领域层(domain):这一层包含了业务领域的所有规则和逻辑。这里会定义
|
||||
- 实体(Entity):
|
||||
- 领域服务(Domain Service
|
||||
- 领域事件 (Event) : 领域事件是领域模型中发生的重要的业务事件,例如用户登录、订单提交等。它们通常表示了领域模型的状态变化,它通常表示了领域模型的状态变化,但它们并`不知道如何处理这些变化`。它们的职责是通知其他部分系统,某个重要的事情发生了。
|
||||
- 仓库接口(Repository Interface)
|
||||
|
||||
更轻量级的领域设计,可以考虑以下策略:
|
||||
- 实体(Entity)
|
||||
- 领域服务(Domain Service)
|
||||
- 领域事件 (Event)
|
||||
|
||||
|
||||
## 领域事件(Domain Events)、领域模型(Domain Model)、UI 三者的关系
|
||||
|
||||
1. UI层接收用户输入:用户通过UI层进行操作,例如点击按钮或填写表单。
|
||||
2. 触发领域事件:根据用户的操作,UI层会触发相应的领域事件。例如,当用户填写表单并点击"登录"按钮时,UI层会触发一个"UserLoggedIn"事件。
|
||||
3. 更新领域模型:领域事件会导致领域模型的状态改变。例如,"UserLoggedIn"事件可能会使User模型的"isLoggedIn"状态变为true。
|
||||
4. UI层反映领域模型的状态:当领域模型的状态改变时,UI层会相应地更新,以反映新的状态。例如,如果User模型的"isLoggedIn"状态变为true,那么UI层可能会显示一个"欢迎回来"的消息,并隐藏"登录"按钮。
|
||||
|
||||
这种方式的好处是,它使得UI层和领域逻辑解耦,使得代码更易于理解和维护。当业务逻辑变化时,你只需要修改领域模型和领域事件,而不需要修改UI层。同样,当UI需求变化时,你只需要修改UI层,而不需要修改领域模型和领域事件。
|
||||
|
||||
## 基本的Demo
|
||||
|
||||
```ts
|
||||
class User {
|
||||
id: string;
|
||||
name: string;
|
||||
isLoggedIn: boolean = false;
|
||||
|
||||
login() {
|
||||
this.isLoggedIn = true;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们定义用户登录的领域事件。这个事件将表示用户登录的动作:
|
||||
|
||||
```ts
|
||||
class UserLoggedIn {
|
||||
constructor(public userId: string) {}
|
||||
}
|
||||
```
|
||||
|
||||
然后,我们需要定义一个领域服务,这个服务将负责处理用户登录的业务逻辑:
|
||||
|
||||
```ts
|
||||
class AuthenticationService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
// 通常,你需要从后端服务验证用户名和密码,这里为了简单起见,我们假设验证总是成功
|
||||
const user = await this.userRepository.findByName(username);
|
||||
user.login();
|
||||
// 通过发布订阅这种方式通知UI更新
|
||||
eventBus.emit('UserLoggedIn',new UserLoggedIn(user.id));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 某个方法 Should 充血到领域模型 Or 放到领域服务
|
||||
|
||||
1. 领域规则:如果方法代表了一个业务规则,那么它应该被放在领域模型中。领域模型负责封装业务规则,保证业务的一致性。例如,User模型可能有一个login方法,因为登录是用户的一个核心业务行为。
|
||||
|
||||
2. 状态变化:如果方法会改变模型的状态,那么它也应该被放在领域模型中。领域模型的状态应该被封装起来,只能通过模型的方法来改变。这样可以保证状态的一致性。
|
||||
|
||||
3. 复杂业务逻辑:如果方法包含了复杂的业务逻辑,那么它可能应该被放在一个领域服务中。领域服务是一种特殊的模型,它负责处理那些涉及多个模型的复杂业务逻辑。例如,一个订单服务(OrderService)可能有一个placeOrder方法,因为下订单涉及到用户、商品和订单等多个模型。
|
||||
|
||||
|
||||
## mobx与发布订阅
|
||||
|
||||
针对 f(领域模型状态)->ui组件,这个通常有如下几种做法:
|
||||
- mobx: makeAutoObserve(this) + mobx-react-lite: <observer/> + useDomain(Context)
|
||||
- 领域服务发布领域事件 + 组件内部订阅最新的领域模型
|
||||
|
||||
ui层使用mobx的`observer()`来订阅反应模型、事件的更新,相比于发布订阅,省去了在组件内部显式的订阅和处理领域事件
|
||||
|
||||
|
||||
## 实例化的两种思量
|
||||
|
||||
构造函数的参数列表中使用了 `public` 关键字,这样 TypeScript 会自动为每个参数创建一个与之同名的类属性,并把构造函数收到的参数值赋给这些属性。
|
||||
|
||||
> 缺点是不能直接接受一个对象作为参数
|
||||
|
||||
```ts
|
||||
export class UserInfo {
|
||||
constructor(
|
||||
public id?: number,
|
||||
public username?: string,
|
||||
public isLoggedIn?: boolean,
|
||||
public phoneNumber?: string | number,
|
||||
public createTime?: number | string,
|
||||
public department?: string,
|
||||
public contactPerson?: string
|
||||
) {}
|
||||
}
|
||||
```
|
||||
如果userInfo对象缺少一些UserInfo类有的属性,这些属性将保持其初始值
|
||||
|
||||
```ts
|
||||
type UserInfoInterface = {
|
||||
id?: number;
|
||||
username?: string;
|
||||
isLoggedIn?: boolean;
|
||||
phoneNumber?: string | number;
|
||||
createTime?: number | string;
|
||||
department?: string;
|
||||
contactPerson?: string;
|
||||
};
|
||||
|
||||
export class UserInfo {
|
||||
id?: number;
|
||||
username?: string;
|
||||
isLoggedIn?: boolean;
|
||||
phoneNumber?: string | number;
|
||||
createTime?: number | string;
|
||||
department?: string;
|
||||
contactPerson?: string;
|
||||
|
||||
constructor(userInfo: UserInfoInterface) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
// 使用 Object.assign 来分配属性
|
||||
Object.assign(this, userInfo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## makeAutoObservable(this)
|
||||
|
||||
1. 需要构造属性声明的最后面
|
||||
2. 实例化之后的属性注入,无法被追踪到
|
143
apps/aorta/core/infra/api/Request.ts
Normal file
143
apps/aorta/core/infra/api/Request.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
import { message } from "antd";
|
||||
import { getFingerprint, SymmetricCrypto } from "@tavi/util";
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
CancelTokenSource,
|
||||
InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
|
||||
interface RequestConfig extends InternalAxiosRequestConfig {
|
||||
onRequestSent?: RequestCallback;
|
||||
}
|
||||
|
||||
type RequestCallback = (token: string) => void;
|
||||
|
||||
type ResponseException = {
|
||||
msg: string;
|
||||
path: string;
|
||||
statusCode: number;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
class AxiosRequestInstance {
|
||||
private instance: AxiosInstance;
|
||||
private cancelTokenMap: Map<string, CancelTokenSource>;
|
||||
|
||||
constructor() {
|
||||
this.instance = axios.create();
|
||||
this.cancelTokenMap = new Map();
|
||||
|
||||
// 请求拦截
|
||||
this.instance.interceptors.request.use(async (config: RequestConfig) => {
|
||||
const token = this.generateToken();
|
||||
const source = axios.CancelToken.source();
|
||||
this.cancelTokenMap.set(token, source);
|
||||
|
||||
// 生成 traceId 并加入请求头
|
||||
const traceId = this.generateTraceId();
|
||||
config.headers["x-trace-id"] = traceId;
|
||||
config.headers["x-request-token"] = token; // 将token加入请求头
|
||||
config.cancelToken = source.token;
|
||||
// 浏览器指纹
|
||||
config.headers["x-finger"] = await getFingerprint();
|
||||
// 浏览器指纹2
|
||||
config.headers["x-finger2"] = new SymmetricCrypto().encrypt({
|
||||
language: navigator.language,
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
screenResolution: `${window.screen.width} x ${window.screen.height}`,
|
||||
});
|
||||
|
||||
if (config.onRequestSent) config.onRequestSent(token);
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截
|
||||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const token = response.headers["x-request-token"] as string;
|
||||
this.cancelTokenMap.delete(token); // 完成后删除此token
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
if (error.response && error.response.status === 403) {
|
||||
const { msg } = error.response.data as ResponseException;
|
||||
message.error(msg);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
private generateToken(): string {
|
||||
return Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
// 生成traceId
|
||||
private generateTraceId(): string {
|
||||
// 使用你自己的 traceId 生成策略,这里是一个简单的示例
|
||||
return Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
// 取消请求
|
||||
public cancelRequest(token: string): void {
|
||||
const source = this.cancelTokenMap.get(token);
|
||||
if (source) {
|
||||
source.cancel("Request canceled, token:" + token);
|
||||
this.cancelTokenMap.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
// 封装get、post等请求
|
||||
public async get<T = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig & { onRequestSent?: RequestCallback }
|
||||
): Promise<T> {
|
||||
const response = await this.instance.get<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: RequestConfig & { onRequestSent?: RequestCallback }
|
||||
): Promise<T> {
|
||||
const response = await this.instance.post<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: RequestConfig & { onRequestSent?: RequestCallback }
|
||||
): Promise<T> {
|
||||
const response = await this.instance.put<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async delete<T = any>(
|
||||
url: string,
|
||||
config?: RequestConfig & { onRequestSent?: RequestCallback }
|
||||
): Promise<T> {
|
||||
const response = await this.instance.delete<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const Request = new AxiosRequestInstance();
|
||||
|
||||
// 使用方法:
|
||||
// 1. 发起请求并获取token
|
||||
// let token: string;
|
||||
// requestInstance.get("https://api.example.com/data", {
|
||||
// onRequestSent: (requestToken) => {
|
||||
// token = requestToken;
|
||||
// },
|
||||
// });
|
||||
|
||||
// 2. 在需要取消请求的时候取消请求
|
||||
// requestInstance.cancelRequest(token);
|
21
apps/aorta/core/infra/api/index.ts
Normal file
21
apps/aorta/core/infra/api/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Request } from "./Request";
|
||||
|
||||
const PREFIX = "/api/aorta";
|
||||
const PREFIX_CERT = "/cert";
|
||||
|
||||
type ResponseType = Promise<{
|
||||
code?: number | string;
|
||||
data?: unknown;
|
||||
msg?: string;
|
||||
}>;
|
||||
|
||||
export const Apis = {
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
signIn: (p: any): ResponseType => Request.post(PREFIX + "/auth/signIn", p),
|
||||
/**
|
||||
* 用户认证
|
||||
*/
|
||||
userAuth: (): ResponseType => Request.get(PREFIX_CERT + "/auth/user"),
|
||||
};
|
1
apps/aorta/core/infra/readme.md
Normal file
1
apps/aorta/core/infra/readme.md
Normal file
|
@ -0,0 +1 @@
|
|||
基础设施层(infrastructure):这一层包含了所有的基础设施服务,例如数据持久化、网络请求等。
|
1
apps/aorta/env/.env.development
vendored
Normal file
1
apps/aorta/env/.env.development
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
REACT_APP_API_URL=https://api-dev.com
|
1
apps/aorta/env/.env.production
vendored
Normal file
1
apps/aorta/env/.env.production
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
REACT_APP_API_URL=https://api-prod.com
|
0
apps/aorta/env/.env.test
vendored
Normal file
0
apps/aorta/env/.env.test
vendored
Normal file
67
apps/aorta/package.json
Normal file
67
apps/aorta/package.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "@tavi/aorta",
|
||||
"version": "1.0.2",
|
||||
"description": "主动脉项目",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development BASE_ENV=development webpack serve -c scripts/webpack.dev.ts",
|
||||
"build": "cross-env NODE_ENV=production BASE_ENV=production webpack -c scripts/webpack.prod.ts",
|
||||
"build:analyze": "cross-env NODE_ENV=production BASE_ENV=production webpack -c scripts/webpack.analyze.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-router": "6.14.1",
|
||||
"react-router-dom": "6.14.1",
|
||||
"axios": "1.3.6",
|
||||
"antd": "5.6.4",
|
||||
"mobx": "6.9.0",
|
||||
"mobx-react-lite": "3.4.3",
|
||||
"react-icons": "4.10.1",
|
||||
"mitt": "3.0.1",
|
||||
"@tavi/i18n": "^1.5.0",
|
||||
"@tavi/util": "1.0.0",
|
||||
"js-cookie": "3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@types/node": "18.16.3",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-router": "5.1.20",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"babel-loader": "^9.1.2",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"core-js": "^3.30.1",
|
||||
"cross-env": "^6.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
"glob-all": "^3.3.1",
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.1.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"prettier": "^2.8.8",
|
||||
"purgecss-webpack-plugin": "^5.0.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"speed-measure-webpack-plugin": "^1.5.0",
|
||||
"style-loader": "^3.3.2",
|
||||
"terser-webpack-plugin": "^5.3.7",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^5.0.2",
|
||||
"webpack-dev-server": "^4.13.3",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"webpackbar": "^5.0.2",
|
||||
"@types/js-cookie": "3.0.3"
|
||||
}
|
||||
}
|
8
apps/aorta/public/favicon.svg
Normal file
8
apps/aorta/public/favicon.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="47" viewBox="0 0 45 47" fill="none">
|
||||
<path
|
||||
d="M24.273 7.7653L19.6087 11.3716L15.3793 5.98863L11 7L30 35.5L34.1633 30.0428L32.7808 28.1213C32.1142 27.1213 31.6711 25.3464 33.2711 24.1464C34.8711 22.9464 36.6061 23.6397 37.4394 24.473L40.7354 28.7305L30 47L0 0H18.0882L24.273 7.7653Z"
|
||||
fill="#2E97D1"></path>
|
||||
<path
|
||||
d="M43.645 9.14625C42.6195 7.84489 40.7332 7.62124 39.4319 8.64673L30.4992 15.6858L28.646 13.3226C27.6235 12.0188 25.7378 11.7907 24.434 12.8132C23.1302 13.8356 22.9022 15.7213 23.9246 17.0251L29.4784 24.1071L29.4954 24.0939L29.506 24.1074L43.1455 13.3594C44.4468 12.3339 44.6705 10.4476 43.645 9.14625Z"
|
||||
fill="#2E97D1"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 725 B |
BIN
apps/aorta/public/img/login_1.webp
Normal file
BIN
apps/aorta/public/img/login_1.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
apps/aorta/public/img/login_mask.webp
Normal file
BIN
apps/aorta/public/img/login_mask.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
17
apps/aorta/public/index.html
Normal file
17
apps/aorta/public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 容器节点 -->
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
11
apps/aorta/public/locales/en-US/translation.json
Normal file
11
apps/aorta/public/locales/en-US/translation.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"LOGIN": {
|
||||
"FORM_ITEM": {
|
||||
"LABEL_1": "USERNAME",
|
||||
"LABEL_2": "PASSWORD",
|
||||
"LABEL_3": "PHONE NUMBER",
|
||||
"LABEL_4": "VERIFY CODE",
|
||||
"BUTTON_1": "LOGIN"
|
||||
}
|
||||
}
|
||||
}
|
16
apps/aorta/public/locales/en/translation.json
Normal file
16
apps/aorta/public/locales/en/translation.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"LOGIN": {
|
||||
"FORM_ITEM": {
|
||||
"LABEL_1": "USERNAME",
|
||||
"LABEL_2": "PASSWORD",
|
||||
"LABEL_3": "PHONE NUMBER",
|
||||
"LABEL_4": "VERIFY CODE",
|
||||
"LABEL_5": "Please enter your username",
|
||||
"LABEL_6": "Please enter the password",
|
||||
"LABEL_7": "Please enter your mobile phone number",
|
||||
"LABEL_8": "Please enter the verification code",
|
||||
"BUTTON_1": "LOGIN"
|
||||
},
|
||||
"FORGET_PASS": "Forgot the password"
|
||||
}
|
||||
}
|
16
apps/aorta/public/locales/zh-CN/translation.json
Normal file
16
apps/aorta/public/locales/zh-CN/translation.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"LOGIN": {
|
||||
"FORM_ITEM": {
|
||||
"LABEL_1": "用户名",
|
||||
"LABEL_2": "密码",
|
||||
"LABEL_3": "手机号",
|
||||
"LABEL_4": "验证码",
|
||||
"LABEL_5": "请输入您的用户名",
|
||||
"LABEL_6": "请输入密码",
|
||||
"LABEL_7": "请输入您的手机号码",
|
||||
"LABEL_8": "请输入验证码",
|
||||
"BUTTON_1": "登录"
|
||||
},
|
||||
"FORGET_PASS": "忘记密码"
|
||||
}
|
||||
}
|
16
apps/aorta/public/locales/zh/translation.json
Normal file
16
apps/aorta/public/locales/zh/translation.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"LOGIN": {
|
||||
"FORM_ITEM": {
|
||||
"LABEL_1": "用户名",
|
||||
"LABEL_2": "密码",
|
||||
"LABEL_3": "手机号",
|
||||
"LABEL_4": "验证码",
|
||||
"LABEL_5": "请输入您的用户名",
|
||||
"LABEL_6": "请输入密码",
|
||||
"LABEL_7": "请输入您的手机号码",
|
||||
"LABEL_8": "请输入验证码",
|
||||
"BUTTON_1": "登录"
|
||||
},
|
||||
"FORGET_PASS": "忘记密码"
|
||||
}
|
||||
}
|
25
apps/aorta/scripts/dev.proxy.ts
Normal file
25
apps/aorta/scripts/dev.proxy.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import WebpackDevServer from "webpack-dev-server";
|
||||
|
||||
type TProxyMap =
|
||||
| WebpackDevServer.ProxyConfigMap
|
||||
| WebpackDevServer.ProxyConfigArrayItem
|
||||
| WebpackDevServer.ProxyConfigArray
|
||||
| undefined;
|
||||
|
||||
export const proxyMap: TProxyMap = {
|
||||
"/dicom-web": {
|
||||
target: "http://localhost:8042/dicom-web/",
|
||||
changeOrigin: true,
|
||||
pathRewrite: { "^/dicom-web": "" },
|
||||
},
|
||||
"/cert": {
|
||||
target: "http://localhost:12144/",
|
||||
changeOrigin: true,
|
||||
pathRewrite: { "^/cert": "/cert" },
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:9008/",
|
||||
changeOrigin: true,
|
||||
pathRewrite: { "^/api": "" },
|
||||
},
|
||||
};
|
4
apps/aorta/scripts/loaders/babel.loader.ts
Normal file
4
apps/aorta/scripts/loaders/babel.loader.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
|
||||
use: "babel-loader"
|
||||
}
|
12
apps/aorta/scripts/loaders/fonts.loader.ts
Normal file
12
apps/aorta/scripts/loaders/fonts.loader.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
test: /.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
|
||||
type: "asset", // type选择asset
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10 * 1024, // 小于10kb转base64
|
||||
}
|
||||
},
|
||||
generator: {
|
||||
filename: 'static/fonts/[name].[contenthash:8][ext]', // 文件输出目录和命名
|
||||
},
|
||||
}
|
12
apps/aorta/scripts/loaders/img.loader.ts
Normal file
12
apps/aorta/scripts/loaders/img.loader.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
test: /\.(png|jpe?g|gif|svg)$/i, // 匹配图片文件
|
||||
type: "asset", // type选择asset
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 20 * 1024, // 小于10kb转base64
|
||||
}
|
||||
},
|
||||
generator: {
|
||||
filename: 'static/images/[name].[contenthash:8][ext]', // 文件输出目录和命名
|
||||
},
|
||||
}
|
9
apps/aorta/scripts/loaders/json.loader.ts
Normal file
9
apps/aorta/scripts/loaders/json.loader.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
// 匹配json文件
|
||||
test: /\.json$/,
|
||||
type: "asset/source", // 将json文件视为文件类型
|
||||
generator: {
|
||||
// 这里专门针对json文件的处理
|
||||
filename: 'static/fonts/[name].[contenthash:8][ext]'
|
||||
}
|
||||
}
|
30
apps/aorta/scripts/loaders/style.loader.ts
Normal file
30
apps/aorta/scripts/loaders/style.loader.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
|
||||
|
||||
const styleLoadersArray = [
|
||||
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
|
||||
{
|
||||
loader: 'css-loader'
|
||||
}
|
||||
]
|
||||
|
||||
export const cssLoader = {
|
||||
test: /.css$/, //匹配 css 文件
|
||||
use: styleLoadersArray
|
||||
}
|
||||
|
||||
export const lessLoader = {
|
||||
test: /\.less$/,
|
||||
use: [
|
||||
...styleLoadersArray,
|
||||
{
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
lessOptions: {
|
||||
// 如果要在less中写类型js的语法,需要加这一个配置
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
155
apps/aorta/scripts/util/openBrowser.js
Normal file
155
apps/aorta/scripts/util/openBrowser.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var chalk = require('chalk');
|
||||
var execSync = require('child_process').execSync;
|
||||
var spawn = require('cross-spawn');
|
||||
var open = require('open');
|
||||
|
||||
// https://github.com/sindresorhus/open#app
|
||||
var OSX_CHROME = 'google chrome';
|
||||
|
||||
const Actions = Object.freeze({
|
||||
NONE: 0,
|
||||
BROWSER: 1,
|
||||
SCRIPT: 2,
|
||||
});
|
||||
|
||||
function getBrowserEnv() {
|
||||
// Attempt to honor this environment variable.
|
||||
// It is specific to the operating system.
|
||||
// See https://github.com/sindresorhus/open#app for documentation.
|
||||
const value = process.env.BROWSER;
|
||||
const args = process.env.BROWSER_ARGS
|
||||
? process.env.BROWSER_ARGS.split(' ')
|
||||
: [];
|
||||
let action;
|
||||
if (!value) {
|
||||
// Default.
|
||||
action = Actions.BROWSER;
|
||||
} else if (value.toLowerCase().endsWith('.js')) {
|
||||
action = Actions.SCRIPT;
|
||||
} else if (value.toLowerCase() === 'none') {
|
||||
action = Actions.NONE;
|
||||
} else {
|
||||
action = Actions.BROWSER;
|
||||
}
|
||||
return { action, value, args };
|
||||
}
|
||||
|
||||
function executeNodeScript(scriptPath, url) {
|
||||
const extraArgs = process.argv.slice(2);
|
||||
const child = spawn(process.execPath, [scriptPath, ...extraArgs, url], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('close', code => {
|
||||
if (code !== 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red(
|
||||
'The script specified as BROWSER environment variable failed.'
|
||||
)
|
||||
);
|
||||
console.log(chalk.cyan(scriptPath) + ' exited with code ' + code + '.');
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function startBrowserProcess(browser, url, args) {
|
||||
// If we're on OS X, the user hasn't specifically
|
||||
// requested a different browser, we can try opening
|
||||
// Chrome with AppleScript. This lets us reuse an
|
||||
// existing tab when possible instead of creating a new one.
|
||||
const shouldTryOpenChromiumWithAppleScript =
|
||||
process.platform === 'darwin' &&
|
||||
(typeof browser !== 'string' || browser === OSX_CHROME);
|
||||
|
||||
if (shouldTryOpenChromiumWithAppleScript) {
|
||||
// Will use the first open browser found from list
|
||||
const supportedChromiumBrowsers = [
|
||||
'Google Chrome Canary',
|
||||
'Google Chrome Dev',
|
||||
'Google Chrome Beta',
|
||||
'Google Chrome',
|
||||
'Microsoft Edge',
|
||||
'Brave Browser',
|
||||
'Vivaldi',
|
||||
'Chromium',
|
||||
];
|
||||
|
||||
for (let chromiumBrowser of supportedChromiumBrowsers) {
|
||||
try {
|
||||
// Try our best to reuse existing tab
|
||||
// on OSX Chromium-based browser with AppleScript
|
||||
execSync('ps cax | grep "' + chromiumBrowser + '"');
|
||||
execSync(
|
||||
'osascript openChrome.applescript "' +
|
||||
encodeURI(url) +
|
||||
'" "' +
|
||||
chromiumBrowser +
|
||||
'"',
|
||||
{
|
||||
cwd: __dirname,
|
||||
stdio: 'ignore',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Another special case: on OS X, check if BROWSER has been set to "open".
|
||||
// In this case, instead of passing `open` to `opn` (which won't work),
|
||||
// just ignore it (thus ensuring the intended behavior, i.e. opening the system browser):
|
||||
// https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768
|
||||
if (process.platform === 'darwin' && browser === 'open') {
|
||||
browser = undefined;
|
||||
}
|
||||
|
||||
// If there are arguments, they must be passed as array with the browser
|
||||
if (typeof browser === 'string' && args.length > 0) {
|
||||
browser = [browser].concat(args);
|
||||
}
|
||||
|
||||
// Fallback to open
|
||||
// (It will always open new tab)
|
||||
try {
|
||||
var options = { app: browser, wait: false, url: true };
|
||||
open(url, options).catch(() => {}); // Prevent `unhandledRejection` error.
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the BROWSER environment variable and decides what to do with it. Returns
|
||||
* true if it opened a browser or ran a node.js script, otherwise false.
|
||||
*/
|
||||
function openBrowser(url) {
|
||||
const { action, value, args } = getBrowserEnv();
|
||||
switch (action) {
|
||||
case Actions.NONE:
|
||||
// Special case: BROWSER="none" will prevent opening completely.
|
||||
return false;
|
||||
case Actions.SCRIPT:
|
||||
return executeNodeScript(value, url);
|
||||
case Actions.BROWSER:
|
||||
return startBrowserProcess(value, url, args);
|
||||
default:
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = openBrowser;
|
94
apps/aorta/scripts/util/openChrome.applescript
Normal file
94
apps/aorta/scripts/util/openChrome.applescript
Normal file
|
@ -0,0 +1,94 @@
|
|||
(*
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
This source code is licensed under the MIT license found in the
|
||||
LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
|
||||
property targetTab: null
|
||||
property targetTabIndex: -1
|
||||
property targetWindow: null
|
||||
property theProgram: "Google Chrome"
|
||||
|
||||
on run argv
|
||||
set theURL to item 1 of argv
|
||||
|
||||
-- Allow requested program to be optional,
|
||||
-- default to Google Chrome
|
||||
if (count of argv) > 1 then
|
||||
set theProgram to item 2 of argv
|
||||
end if
|
||||
|
||||
using terms from application "Google Chrome"
|
||||
tell application theProgram
|
||||
|
||||
if (count every window) = 0 then
|
||||
make new window
|
||||
end if
|
||||
|
||||
-- 1: Looking for tab running debugger
|
||||
-- then, Reload debugging tab if found
|
||||
-- then return
|
||||
set found to my lookupTabWithUrl(theURL)
|
||||
if found then
|
||||
set targetWindow's active tab index to targetTabIndex
|
||||
tell targetTab to reload
|
||||
tell targetWindow to activate
|
||||
set index of targetWindow to 1
|
||||
return
|
||||
end if
|
||||
|
||||
-- 2: Looking for Empty tab
|
||||
-- In case debugging tab was not found
|
||||
-- We try to find an empty tab instead
|
||||
set found to my lookupTabWithUrl("chrome://newtab/")
|
||||
if found then
|
||||
set targetWindow's active tab index to targetTabIndex
|
||||
set URL of targetTab to theURL
|
||||
tell targetWindow to activate
|
||||
return
|
||||
end if
|
||||
|
||||
-- 3: Create new tab
|
||||
-- both debugging and empty tab were not found
|
||||
-- make a new tab with url
|
||||
tell window 1
|
||||
activate
|
||||
make new tab with properties {URL:theURL}
|
||||
end tell
|
||||
end tell
|
||||
end using terms from
|
||||
end run
|
||||
|
||||
-- Function:
|
||||
-- Lookup tab with given url
|
||||
-- if found, store tab, index, and window in properties
|
||||
-- (properties were declared on top of file)
|
||||
on lookupTabWithUrl(lookupUrl)
|
||||
using terms from application "Google Chrome"
|
||||
tell application theProgram
|
||||
-- Find a tab with the given url
|
||||
set found to false
|
||||
set theTabIndex to -1
|
||||
repeat with theWindow in every window
|
||||
set theTabIndex to 0
|
||||
repeat with theTab in every tab of theWindow
|
||||
set theTabIndex to theTabIndex + 1
|
||||
if (theTab's URL as string) contains lookupUrl then
|
||||
-- assign tab, tab index, and window to properties
|
||||
set targetTab to theTab
|
||||
set targetTabIndex to theTabIndex
|
||||
set targetWindow to theWindow
|
||||
set found to true
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
|
||||
if found then
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
end using terms from
|
||||
return found
|
||||
end lookupTabWithUrl
|
18
apps/aorta/scripts/webpack.analyze.ts
Normal file
18
apps/aorta/scripts/webpack.analyze.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Configuration } from "webpack";
|
||||
import { merge } from "webpack-merge";
|
||||
import prodConfig from "./webpack.prod";
|
||||
|
||||
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
|
||||
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
|
||||
|
||||
// 引入webpack打包速度分析插件
|
||||
const smp = new SpeedMeasurePlugin();
|
||||
|
||||
// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
|
||||
const analyConfig: Configuration = smp.wrap(merge(prodConfig, {
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin() // 配置分析打包结果插件
|
||||
]
|
||||
}))
|
||||
|
||||
export default analyConfig;
|
74
apps/aorta/scripts/webpack.common.ts
Normal file
74
apps/aorta/scripts/webpack.common.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { Configuration, DefinePlugin } from "webpack";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import path from "path";
|
||||
import * as dotenv from "dotenv";
|
||||
import fontsLoader from "./loaders/fonts.loader";
|
||||
import jsonLoader from "./loaders/json.loader";
|
||||
import imgLoader from "./loaders/img.loader";
|
||||
import babelLoader from "./loaders/babel.loader";
|
||||
import { cssLoader, lessLoader } from "./loaders/style.loader";
|
||||
|
||||
// 加载配置文件
|
||||
const envConfig = dotenv.config({
|
||||
path: path.resolve(__dirname, "../env/.env." + process.env.BASE_ENV),
|
||||
});
|
||||
|
||||
const baseConfig: Configuration = {
|
||||
entry: path.join(__dirname, "../src/index.tsx"), // 入口文件
|
||||
// 打包出口文件
|
||||
output: {
|
||||
filename: "static/js/[name].[chunkhash:8].js", // 每个输出js的名称
|
||||
path: path.join(__dirname, "../dist"), // 打包结果输出路径
|
||||
clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
|
||||
publicPath: "/", // 打包后文件的公共前缀路径
|
||||
},
|
||||
// loader 配置
|
||||
module: {
|
||||
rules: [
|
||||
babelLoader,
|
||||
cssLoader,
|
||||
lessLoader,
|
||||
jsonLoader,
|
||||
imgLoader,
|
||||
fontsLoader,
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
||||
// 别名需要配置两个地方,这里和 tsconfig.json
|
||||
alias: {
|
||||
"@": path.join(__dirname, "../src"),
|
||||
"@@": path.join(__dirname, "../core"),
|
||||
},
|
||||
// fallback: {
|
||||
// crypto: require.resolve('crypto-browserify'),
|
||||
// buffer: require.resolve("buffer/"),
|
||||
// http: require.resolve("stream-http"),
|
||||
// },
|
||||
},
|
||||
// plugins
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
// 复制 'index.html' 文件,并自动引入打包输出的所有资源(js/css)
|
||||
template: path.join(__dirname, "../public/index.html"),
|
||||
inject: true, // 自动注入静态资源
|
||||
hash: true,
|
||||
cache: false,
|
||||
// 压缩html资源
|
||||
minify: {
|
||||
removeAttributeQuotes: true,
|
||||
collapseWhitespace: true, //去空格
|
||||
removeComments: true, // 去注释
|
||||
minifyJS: true, // 在脚本元素和事件属性中缩小JavaScript(使用UglifyJS)
|
||||
minifyCSS: true, // 缩小CSS样式元素和样式属性
|
||||
},
|
||||
}),
|
||||
new DefinePlugin({
|
||||
"process.env": JSON.stringify(envConfig.parsed),
|
||||
"process.env.BASE_ENV": JSON.stringify(process.env.BASE_ENV),
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default baseConfig;
|
66
apps/aorta/scripts/webpack.dev.ts
Normal file
66
apps/aorta/scripts/webpack.dev.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import path from "path";
|
||||
import { merge } from "webpack-merge";
|
||||
import webpack, { Configuration as WebpackConfiguration } from "webpack";
|
||||
import WebpackDevServer from "webpack-dev-server";
|
||||
import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||
import baseConfig from "./webpack.common";
|
||||
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
|
||||
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
|
||||
import { proxyMap } from "./dev.proxy";
|
||||
|
||||
/**
|
||||
* 运行命令的时候重启一次打开一个tab 页很烦,所以呢优化一下
|
||||
* 参考:create-react-app 的启动方式
|
||||
* https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openChrome.applescript
|
||||
* 记得关闭webpack-dev-server的配置中的自动打开 open: false 或者注释
|
||||
*/
|
||||
const openBrowser = require("./util/openBrowser");
|
||||
|
||||
interface Configuration extends WebpackConfiguration {
|
||||
devServer?: WebpackDevServerConfiguration;
|
||||
}
|
||||
|
||||
const host = "127.0.0.1";
|
||||
const port = 9000;
|
||||
|
||||
// 合并公共配置,并添加开发环境配置
|
||||
const devConfig: Configuration = merge(baseConfig, {
|
||||
mode: "development", // 开发模式,打包更加快速,省了代码优化步骤
|
||||
/**
|
||||
* 开发环境推荐:eval-cheap-module-source-map
|
||||
* 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
|
||||
* 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
|
||||
* 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
|
||||
*/
|
||||
devtool: "eval-cheap-module-source-map",
|
||||
stats: "errors-only",
|
||||
plugins: [
|
||||
new ReactRefreshWebpackPlugin(), // 添加热更新插件
|
||||
new FriendlyErrorsWebpackPlugin(),
|
||||
],
|
||||
});
|
||||
|
||||
const devServer = new WebpackDevServer(
|
||||
{
|
||||
host, // 地址
|
||||
port, // 端口
|
||||
open: false, // 是否自动打开,关闭
|
||||
setupExitSignals: true, // 允许在 SIGINT 和 SIGTERM 信号时关闭开发服务器和退出进程。
|
||||
compress: false, // gzip压缩,开发环境不开启,提升热更新速度
|
||||
hot: true, // 开启热更新,后面会讲react模块热替换具体配置
|
||||
historyApiFallback: true, // 解决history路由404问题
|
||||
static: {
|
||||
directory: path.join(__dirname, "../public"), // 托管静态资源public文件夹
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
proxy: proxyMap,
|
||||
},
|
||||
webpack(devConfig)
|
||||
);
|
||||
|
||||
devServer.start().then(() => {
|
||||
// 启动界面
|
||||
openBrowser(`http://${host}:${port}`);
|
||||
});
|
||||
|
||||
export default devConfig;
|
103
apps/aorta/scripts/webpack.prod.ts
Normal file
103
apps/aorta/scripts/webpack.prod.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { Configuration } from "webpack";
|
||||
import { merge } from "webpack-merge";
|
||||
import baseConfig from "./webpack.common";
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import CompressionPlugin from "compression-webpack-plugin";
|
||||
const globAll = require("glob-all");
|
||||
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
|
||||
import path from "path";
|
||||
import CopyWebpackPlugin from "copy-webpack-plugin";
|
||||
|
||||
const prodConfig: Configuration = merge(baseConfig, {
|
||||
mode: "production", // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new CssMinimizerPlugin(), // 压缩css
|
||||
// 压缩js
|
||||
new TerserPlugin({
|
||||
parallel: true, // 开启多线程压缩
|
||||
terserOptions: {
|
||||
compress: {
|
||||
pure_funcs: ["console.log"], // 删除console.log
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
splitChunks: {
|
||||
// 分隔代码
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
// 提取node_modules代码
|
||||
test: /node_modules/, // 只匹配node_modules里面的模块
|
||||
name: "vendors", // 提取文件命名为vendors,js后缀和chunkhash会自动加
|
||||
minChunks: 1, // 只要使用一次就提取出来
|
||||
chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
|
||||
minSize: 0, // 提取代码体积大于0就提取出来
|
||||
priority: 1, // 提取优先级为1
|
||||
},
|
||||
commons: {
|
||||
// 提取页面公共代码
|
||||
name: "commons", // 提取文件命名为commons
|
||||
minChunks: 2, // 只要使用两次就提取出来
|
||||
chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
|
||||
minSize: 0, // 提取代码体积大于0就提取出来
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
performance: {
|
||||
hints: false,
|
||||
maxAssetSize: 4000000, // 整数类型(以字节为单位)
|
||||
maxEntrypointSize: 5000000, // 整数类型(以字节为单位)
|
||||
},
|
||||
plugins: [
|
||||
// 抽离css
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "static/css/[name].[contenthash:8].css", // 抽离css的输出目录和名称
|
||||
}),
|
||||
// 清理无用css,检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称
|
||||
// 只打包这些文件中用到的样式
|
||||
new PurgeCSSPlugin({
|
||||
paths: globAll.sync(
|
||||
[
|
||||
`${path.join(__dirname, "../src")}/**/*`,
|
||||
path.join(__dirname, "../public/index.html"),
|
||||
],
|
||||
{
|
||||
nodir: true,
|
||||
}
|
||||
),
|
||||
// 用 only 来指定 purgecss-webpack-plugin 的入口
|
||||
// https://github.com/FullHuman/purgecss/tree/main/packages/purgecss-webpack-plugin
|
||||
only: ["dist"],
|
||||
safelist: {
|
||||
standard: [/^ant-/], // 过滤以ant-开头的类名,哪怕没用到也不删除
|
||||
},
|
||||
}),
|
||||
// 打包时生成gzip文件,减少nginx的gzip实时压缩负载
|
||||
new CompressionPlugin({
|
||||
test: /\.(js|css)$/, // 只生成css,js压缩文件
|
||||
filename: "[path][base].gz", // 文件命名
|
||||
algorithm: "gzip", // 压缩格式,默认是gzip
|
||||
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
|
||||
minRatio: 0.8, // 压缩率,默认值是 0.8
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, "../public/img"),
|
||||
to: "./img",
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, "../public/favicon.svg"),
|
||||
to: ".",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default prodConfig;
|
28
apps/aorta/src/App.tsx
Normal file
28
apps/aorta/src/App.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { DomainServiceProvider } from "./context/domainService";
|
||||
import { defaultLang, theme } from "./constant";
|
||||
import { useTracker } from "./hook/useTracker";
|
||||
import { RouterElements } from "./router";
|
||||
import { ConfigProvider } from "antd";
|
||||
import { useI18n } from "@tavi/i18n";
|
||||
import "./styles/app.less";
|
||||
|
||||
/**
|
||||
* 国际化
|
||||
*/
|
||||
export const { i18n } = useI18n({
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
fallbackLng: defaultLang,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
export const App = () => {
|
||||
useTracker();
|
||||
|
||||
return (
|
||||
<DomainServiceProvider>
|
||||
<ConfigProvider theme={theme}>
|
||||
<RouterElements />
|
||||
</ConfigProvider>
|
||||
</DomainServiceProvider>
|
||||
);
|
||||
};
|
54
apps/aorta/src/components/LanguageSelect.tsx
Normal file
54
apps/aorta/src/components/LanguageSelect.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { AiOutlineGlobal } from "react-icons/ai";
|
||||
import { i18n } from "@/App";
|
||||
import { ConfigProvider, Select } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { defaultLang } from "@/constant";
|
||||
|
||||
interface LanguageSelectProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const langOptions = [
|
||||
{ value: "zh-CN", label: "中文(Chinese)" },
|
||||
{ value: "en-US", label: "English" },
|
||||
];
|
||||
|
||||
export const LanguageSelect = (props: LanguageSelectProps) => {
|
||||
const [language, setLanguage] = useState<string>();
|
||||
|
||||
useEffect(() => setLanguage(i18n?.language ?? defaultLang), []);
|
||||
|
||||
const onLanguageChange = (v: string) => {
|
||||
i18n.changeLanguage(v);
|
||||
setLanguage(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#585963",
|
||||
colorBorder: "#585963",
|
||||
colorBgBase: "transparent",
|
||||
colorTextBase: "#fff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<AiOutlineGlobal
|
||||
style={{
|
||||
fontSize: "1.71rem",
|
||||
color: "#585963",
|
||||
marginRight: ".25rem",
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
size="small"
|
||||
value={language}
|
||||
onChange={onLanguageChange}
|
||||
options={langOptions}
|
||||
/>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
14
apps/aorta/src/components/Layout/index.tsx
Normal file
14
apps/aorta/src/components/Layout/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Outlet } from "react-router";
|
||||
|
||||
interface LayoutProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const Layout = (props: LayoutProps) => {
|
||||
return (
|
||||
<div>
|
||||
<header>12222</header>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
54
apps/aorta/src/components/Logo.tsx
Normal file
54
apps/aorta/src/components/Logo.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { useNavigate } from "react-router";
|
||||
|
||||
interface LogoProps {
|
||||
children?: JSX.Element;
|
||||
/**
|
||||
* 是否可以点击
|
||||
*/
|
||||
isClicked?: boolean;
|
||||
}
|
||||
|
||||
export const Logo = (props: LogoProps) => {
|
||||
const { isClicked = true } = props;
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: isClicked ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => isClicked && navigate("/list")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="45"
|
||||
height="47"
|
||||
viewBox="0 0 45 47"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M24.273 7.7653L19.6087 11.3716L15.3793 5.98863L11 7L30 35.5L34.1633 30.0428L32.7808 28.1213C32.1142 27.1213 31.6711 25.3464 33.2711 24.1464C34.8711 22.9464 36.6061 23.6397 37.4394 24.473L40.7354 28.7305L30 47L0 0H18.0882L24.273 7.7653Z"
|
||||
fill="#2E97D1"
|
||||
/>
|
||||
<path
|
||||
d="M43.645 9.14625C42.6195 7.84489 40.7332 7.62124 39.4319 8.64673L30.4992 15.6858L28.646 13.3226C27.6235 12.0188 25.7378 11.7907 24.434 12.8132C23.1302 13.8356 22.9022 15.7213 23.9246 17.0251L29.4784 24.1071L29.4954 24.0939L29.506 24.1074L43.1455 13.3594C44.4468 12.3339 44.6705 10.4476 43.645 9.14625Z"
|
||||
fill="#2E97D1"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
style={{ paddingLeft: 12 }}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="95"
|
||||
height="26"
|
||||
viewBox="0 0 95 26"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.08 18.8C1.89333 18.8 1.72 18.7667 1.56 18.7C1.4 18.6333 1.26 18.54 1.14 18.42C1.03333 18.3 0.96 18.16 0.92 18C0.88 17.8267 0.886667 17.6467 0.94 17.46L2.28 7.48H14.38L14.18 9.4H5.74L4.78 16.88H12.26L13.22 16.12L12.84 18.8H2.08ZM18.4758 18.8L16.3558 8.04L15.9758 7.48H20.0158L21.5358 15.34L25.3758 7.48H29.2158L23.8558 18.8H18.4758ZM44.1595 7.48L43.3995 13.62C43.2929 14.0067 43.0795 14.3267 42.7595 14.58C42.4529 14.8333 42.1529 14.96 41.8595 14.96H35.1395L34.5595 18.8H30.9195L32.2595 8.04L31.6795 7.48H44.1595ZM35.9195 9.4L35.5195 13.04H39.5595L40.1395 9.4H35.9195ZM51.3409 7.48L49.6009 18.8H45.9609L47.3009 8.24L46.7209 7.48H51.3409ZM58.4238 7.48L57.0838 16.88H64.5638L64.3838 18.8H54.4038C54.0171 18.8 53.7238 18.6733 53.5238 18.42C53.3371 18.1667 53.2438 17.8467 53.2438 17.46L54.4038 8.24L53.8238 7.48H58.4238ZM78.1963 7.48C78.3829 7.48 78.5563 7.51333 78.7163 7.58C78.8763 7.64667 79.0096 7.74 79.1163 7.86C79.2363 7.98 79.3163 8.12 79.3563 8.28C79.3963 8.44 79.3896 8.62 79.3363 8.82L77.9963 18.8H67.2363C67.0496 18.8 66.8763 18.7667 66.7163 18.7C66.5563 18.6333 66.4163 18.54 66.2963 18.42C66.1896 18.3 66.1163 18.16 66.0763 18C66.0363 17.8267 66.0429 17.6467 66.0963 17.46L67.4363 7.48H78.1963ZM70.8963 9.4L69.9363 16.88H74.3563L75.4963 9.4H70.8963ZM84.0534 18.8L85.3934 9.4H81.1734L81.3734 7.48H93.6534L93.2734 9.4H89.2334L87.8934 18.8H84.0534Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
27
apps/aorta/src/constant.tsx
Normal file
27
apps/aorta/src/constant.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { ThemeConfig } from "antd";
|
||||
|
||||
/**
|
||||
* requireAuth重定向路由
|
||||
*/
|
||||
export const AuthFailedReplacePath = "/login";
|
||||
|
||||
/**
|
||||
* router/router.config.tsx忘记配置路由的meta.title时默认使用
|
||||
*/
|
||||
export const defaultTitle = "CVPILOT Viewer";
|
||||
|
||||
/**
|
||||
* token优先级低于局部ConfigProvider
|
||||
*/
|
||||
export const theme: ThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: "#2e97d1",
|
||||
borderRadius: 2,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认语言
|
||||
* @enum "zh-CN" | 'en' | 'en-US' | 'zh'
|
||||
*/
|
||||
export const defaultLang = "zh-CN";
|
33
apps/aorta/src/context/auth.tsx
Normal file
33
apps/aorta/src/context/auth.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
type Auth = {
|
||||
isLoggedIn: boolean;
|
||||
};
|
||||
|
||||
type AuthContextType = {
|
||||
auth: Auth;
|
||||
setAuth: React.Dispatch<React.SetStateAction<Auth>>;
|
||||
};
|
||||
|
||||
const defaultAuth: AuthContextType["auth"] = { isLoggedIn: false };
|
||||
const defaultSetAuth: AuthContextType["setAuth"] = () => {};
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>({
|
||||
auth: defaultAuth,
|
||||
setAuth: defaultSetAuth,
|
||||
});
|
||||
|
||||
interface AuthProviderProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const AuthProvider = (props: AuthProviderProps) => {
|
||||
const [auth, setAuth] = useState<Auth>({ isLoggedIn: false });
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ auth, setAuth }}>
|
||||
{props.children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
25
apps/aorta/src/context/domainService.tsx
Normal file
25
apps/aorta/src/context/domainService.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { UserRepository } from "@@/domain/User/UserRepository";
|
||||
import { UserService } from "@@/domain/User/UserService";
|
||||
import { createContext } from "react";
|
||||
|
||||
export type Services = {
|
||||
userDomainService: UserService;
|
||||
};
|
||||
|
||||
const defaultServiceMap = {
|
||||
userDomainService: new UserService(new UserRepository()),
|
||||
};
|
||||
|
||||
export const DomainServiceContext = createContext<Services>(defaultServiceMap);
|
||||
|
||||
interface DomainServiceProviderProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const DomainServiceProvider = (props: DomainServiceProviderProps) => {
|
||||
return (
|
||||
<DomainServiceContext.Provider value={defaultServiceMap}>
|
||||
{props.children}
|
||||
</DomainServiceContext.Provider>
|
||||
);
|
||||
};
|
4
apps/aorta/src/hook/useAuth.tsx
Normal file
4
apps/aorta/src/hook/useAuth.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { useContext } from "react";
|
||||
import { AuthContext } from "../context/auth";
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
10
apps/aorta/src/hook/useDomain.tsx
Normal file
10
apps/aorta/src/hook/useDomain.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { DomainServiceContext, Services } from "@/context/domainService";
|
||||
import { useContext } from "react";
|
||||
|
||||
export const useDomain = (): Services => {
|
||||
const context = useContext(DomainServiceContext);
|
||||
if (context === null) {
|
||||
throw new Error("useDomain ctx丢失,必须在domainService上下文中使用");
|
||||
}
|
||||
return context;
|
||||
};
|
31
apps/aorta/src/hook/useTracker.tsx
Normal file
31
apps/aorta/src/hook/useTracker.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { SDK_Logger } from "@tavi/util";
|
||||
|
||||
const sdk_logger = new SDK_Logger({
|
||||
app_id: "aorta",
|
||||
tracking_attr: "data-tracking-id",
|
||||
});
|
||||
window.sdk_logger = sdk_logger;
|
||||
|
||||
export const useTracker = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("NODE_ENV", process.env.NODE_ENV);
|
||||
console.log("BASE_ENV", process.env.BASE_ENV);
|
||||
console.log("process.env", process.env);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
document.body.addEventListener("click", (e) => {
|
||||
const id = sdk_logger.findTrackingId(e.target as HTMLElement | null);
|
||||
console.log("id", id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname) {
|
||||
console.log(location.pathname);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
};
|
11
apps/aorta/src/index.tsx
Normal file
11
apps/aorta/src/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
const root = document.querySelector("#root");
|
||||
import { App } from "./App";
|
||||
|
||||
if (root)
|
||||
createRoot(root).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
18
apps/aorta/src/modules/Dashboard/index.tsx
Normal file
18
apps/aorta/src/modules/Dashboard/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Button } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
interface DashboardProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const Dashboard = (props: DashboardProps) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<Button onClick={() => navigate("/list")} data-tracking-id="test-btn">
|
||||
跳转list
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
159
apps/aorta/src/modules/Login/LoginForm.tsx
Normal file
159
apps/aorta/src/modules/Login/LoginForm.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { Button, ConfigProvider, Form, Input } from "antd";
|
||||
import { useTranslation } from "@tavi/i18n";
|
||||
|
||||
import {
|
||||
AiOutlineUser,
|
||||
AiOutlineMobile,
|
||||
AiOutlineLock,
|
||||
AiOutlineSafetyCertificate,
|
||||
} from "react-icons/ai";
|
||||
import { REG } from "@tavi/util";
|
||||
|
||||
export interface FormFields {
|
||||
username?: string;
|
||||
password?: string;
|
||||
phoneNumber?: number;
|
||||
verifyCode?: number;
|
||||
}
|
||||
interface LoginFormProps {
|
||||
children?: JSX.Element;
|
||||
styles?: React.CSSProperties;
|
||||
onFormChange?: (v: FormFields) => void;
|
||||
onLogin: (v: FormFields) => void;
|
||||
}
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
props.onLogin(form.getFieldsValue());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = () => {
|
||||
if (props.onFormChange) props?.onFormChange(form.getFieldsValue());
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="login-form-group"
|
||||
layout="vertical"
|
||||
onChange={handleFormChange}
|
||||
form={form}
|
||||
style={{ ...props.styles }}
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#585963",
|
||||
colorBorder: "#585963",
|
||||
colorBgBase: "transparent",
|
||||
colorTextBase: "#fff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label={t("LOGIN.FORM_ITEM.LABEL_1")}
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: "请输入用户名",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<AiOutlineUser />}
|
||||
placeholder={t("LOGIN.FORM_ITEM.LABEL_5") as string}
|
||||
onPressEnter={handleLogin}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("LOGIN.FORM_ITEM.LABEL_2")}
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: REG.password,
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: "6-20位,包含 大写、小写、字母、特殊字符",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<AiOutlineLock />}
|
||||
placeholder={t("LOGIN.FORM_ITEM.LABEL_6") as string}
|
||||
onPressEnter={handleLogin}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("LOGIN.FORM_ITEM.LABEL_3")}
|
||||
name="phoneNumber"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: REG.phoneNumber,
|
||||
min: 11,
|
||||
max: 11,
|
||||
message: "请输入正确的手机号",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<AiOutlineMobile />}
|
||||
placeholder={t("LOGIN.FORM_ITEM.LABEL_7") as string}
|
||||
onPressEnter={handleLogin}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("LOGIN.FORM_ITEM.LABEL_4")}
|
||||
name="verifyCode"
|
||||
style={{ marginBottom: "4.29rem" }}
|
||||
rules={[
|
||||
{
|
||||
min: 6,
|
||||
max: 6,
|
||||
message: "请输入验证码",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<AiOutlineSafetyCertificate />}
|
||||
placeholder={t("LOGIN.FORM_ITEM.LABEL_8") as string}
|
||||
onPressEnter={handleLogin}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#2E97D1",
|
||||
colorTextBase: "#fff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button size="large" block type="primary" onClick={handleLogin}>
|
||||
{t("LOGIN.FORM_ITEM.BUTTON_1")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</ConfigProvider>
|
||||
</Form>
|
||||
);
|
||||
};
|
52
apps/aorta/src/modules/Login/index.less
Normal file
52
apps/aorta/src/modules/Login/index.less
Normal file
|
@ -0,0 +1,52 @@
|
|||
.login-wrapper {
|
||||
position: relative;
|
||||
background: #1f2a34;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
.illustration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: right bottom;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 50% 80%;
|
||||
|
||||
>aside {
|
||||
padding: 5.71rem 4.29rem;
|
||||
position: absolute;
|
||||
background: #050407;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 34.29rem;
|
||||
|
||||
.ant-form-item-label label {
|
||||
color: #585963;
|
||||
}
|
||||
|
||||
>header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 5rem;
|
||||
|
||||
.lang-select-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
>footer {
|
||||
.forget-pass {
|
||||
color: hsl(235, 6%, 37%);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: hsl(235, 6%, 57%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
apps/aorta/src/modules/Login/index.tsx
Normal file
71
apps/aorta/src/modules/Login/index.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { LanguageSelect } from "@/components/LanguageSelect";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { FormFields, LoginForm } from "./LoginForm";
|
||||
import "./index.less";
|
||||
import { useDomain } from "@/hook/useDomain";
|
||||
import { useTranslation } from "@tavi/i18n";
|
||||
import { Observer, observer } from "mobx-react-lite";
|
||||
import { UserService } from "@@/domain/User/UserService";
|
||||
import { useNavigate } from "react-router";
|
||||
import { message } from "antd";
|
||||
|
||||
interface LoginProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
// const TestComponent: React.FC<{ domainService: UserService }> = observer(
|
||||
// ({ domainService }) => (
|
||||
// <div style={{ color: "#fff" }}>
|
||||
// <p>name: {domainService.user.isLoggedIn ? "登录" : "未登录"}</p>
|
||||
// </div>
|
||||
// )
|
||||
// );
|
||||
|
||||
export const Login = (props: LoginProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const { userDomainService } = useDomain();
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
const onLogin = async (v: FormFields) => {
|
||||
const { success, msg } = await userDomainService.handleLogin(v);
|
||||
if (success) {
|
||||
messageApi.success(msg);
|
||||
navigate("/");
|
||||
} else {
|
||||
messageApi.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="login-wrapper"
|
||||
style={{ backgroundImage: `url('/img/login_mask.webp')` }}
|
||||
>
|
||||
<div
|
||||
className="illustration"
|
||||
style={{ backgroundImage: `url('/img/login_1.webp')` }}
|
||||
>
|
||||
<aside>
|
||||
{contextHolder}
|
||||
<header>
|
||||
<Logo isClicked={false} />
|
||||
<LanguageSelect />
|
||||
</header>
|
||||
{/* <TestComponent domainService={userDomainService} /> */}
|
||||
<LoginForm
|
||||
styles={{ width: "100%" }}
|
||||
onFormChange={(v) => userDomainService.updateLoginForm(v)}
|
||||
onLogin={onLogin}
|
||||
/>
|
||||
<footer style={{ paddingTop: "6.64rem", color: "#585963" }}>
|
||||
<span className="forget-pass">{t("LOGIN.FORGET_PASS")}</span>
|
||||
</footer>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
7
apps/aorta/src/modules/PatientList/index.tsx
Normal file
7
apps/aorta/src/modules/PatientList/index.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
interface PatientListProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const PatientList = (props: PatientListProps) => {
|
||||
return <div>PatientList</div>;
|
||||
};
|
7
apps/aorta/src/modules/Peripheral/index.tsx
Normal file
7
apps/aorta/src/modules/Peripheral/index.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
interface PeripheralViewerProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const PeripheralViewer = (props: PeripheralViewerProps) => {
|
||||
return <div>PeripheralViewer</div>;
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
.heading {
|
||||
&.l2 {
|
||||
padding-left: .29rem;
|
||||
font-size: 1.14rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: var(--color-bg-primary);
|
||||
border-left: .29rem solid var(--color-bg-primary);
|
||||
margin-bottom: 1.14rem;
|
||||
|
||||
span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.l1 {
|
||||
position: relative;
|
||||
font-size: 1.7rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
bottom: -.5rem;
|
||||
width: 3.36rem;
|
||||
height: 0.21rem;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import './index.less'
|
||||
|
||||
interface HeadingProps {
|
||||
title: string
|
||||
forRef?: boolean
|
||||
style?: React.CSSProperties
|
||||
children?: JSX.Element
|
||||
level?: 1 | 2
|
||||
}
|
||||
|
||||
export const Heading = (props: HeadingProps) => {
|
||||
const levelMapping = {
|
||||
1: (
|
||||
<div className='heading l1' style={props.style}>
|
||||
{props.title}
|
||||
</div>
|
||||
),
|
||||
2: (
|
||||
<div className='heading l2' style={props.style}>
|
||||
{props.title}
|
||||
{props.forRef && <span>(仅供参考)</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return levelMapping[props.level ?? 2]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
.image-card {
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
height: 1.71rem;
|
||||
line-height: 1.71rem;
|
||||
background: #F5F5F5;
|
||||
border: 0.07rem solid #8A8A8A;
|
||||
text-align: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
min-height: 6rem;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border: 2px dashed var(--color-bg-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import "./index.less";
|
||||
import axios from "axios";
|
||||
|
||||
interface ImageItemProps {
|
||||
children?: JSX.Element;
|
||||
src?: string;
|
||||
imgStyle?: React.CSSProperties;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const ImageItem = (props: ImageItemProps) => {
|
||||
const [actionVisible, setActionVisible] = useState(false);
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
const actionRef = useRef<HTMLElement | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [imgSrc, setImgSrc] = useState(props.src);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: any) => {
|
||||
if (actionRef.current?.contains(e.target)) return e.preventDefault();
|
||||
if (imgRef.current && !imgRef.current.contains(e.target))
|
||||
setActionVisible(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.addEventListener("mousedown", handleClickOutside);
|
||||
}, [imgRef]);
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*/
|
||||
const onDelete = () => {
|
||||
setImgSrc("");
|
||||
fileInputRef.current?.setAttribute("value", "");
|
||||
};
|
||||
3;
|
||||
const onInsert = () => fileInputRef.current?.click();
|
||||
|
||||
const handleFileChange = (event: any) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const fd = new FormData();
|
||||
fd.append("file", event.target.files[0]);
|
||||
axios
|
||||
.post("/api/report/upload", fd, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
})
|
||||
.then((res) => {
|
||||
const { objectName } = res.data;
|
||||
setImgSrc(`/api/report/img/${objectName}`);
|
||||
});
|
||||
//TODO API接口上传,THEN
|
||||
setImgSrc(event.target.result);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-card">
|
||||
<p>{props.title}</p>
|
||||
{!imgSrc ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setActionVisible(true)}
|
||||
className={`img ${actionVisible ? "active" : ""}`}
|
||||
ref={imgRef}
|
||||
style={{ ...props.imgStyle, backgroundImage: `url(${imgSrc})` }}
|
||||
></div>
|
||||
)}
|
||||
{actionVisible && (
|
||||
<section ref={actionRef}>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*" // 接受任何类型的图片文件
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: "none" }} // 隐藏真正的文件输入元素
|
||||
/>
|
||||
{!!imgSrc && <button onClick={onDelete}>删除</button>}
|
||||
<button onClick={onInsert}>插入</button>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
.page-foot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
border-top: 0.07rem solid var(--color-bg-primary);
|
||||
padding-top: 0.57rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import "./index.less";
|
||||
|
||||
export interface PaperFootProps {
|
||||
children?: JSX.Element;
|
||||
date?: string;
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
pageNum?: number;
|
||||
/**
|
||||
* 全部页码
|
||||
*/
|
||||
totalPageNum?: number;
|
||||
}
|
||||
|
||||
export const PaperFoot = (props: PaperFootProps) => {
|
||||
return (
|
||||
<div className="page-foot">
|
||||
<div>
|
||||
{props.date && (
|
||||
<>
|
||||
报告日期: <b>{props.date}</b>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{props.pageNum && props.totalPageNum && (
|
||||
<>
|
||||
第<b>{props.pageNum}</b>页/共<b>{props.totalPageNum}</b>页
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
.page-head {
|
||||
text-align: right;
|
||||
line-height: 1;
|
||||
padding-bottom: 0.57rem;
|
||||
border-bottom: 0.07rem solid var(--color-bg-primary);
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.logo-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import "./index.less";
|
||||
|
||||
interface PaperHeadProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const PaperHead = (props: PaperHeadProps) => {
|
||||
return (
|
||||
<div className="page-head">
|
||||
<div className="logo-group">
|
||||
<svg
|
||||
viewBox="0 0 45 47"
|
||||
style={{ height: "1.71rem", marginRight: ".57rem" }}
|
||||
>
|
||||
<path
|
||||
d="M24.273 7.7653L19.6087 11.3716L15.3793 5.98863L11 7L30 35.5L34.1633 30.0428L32.7808 28.1213C32.1142 27.1213 31.6711 25.3464 33.2711 24.1464C34.8711 22.9464 36.6061 23.6397 37.4394 24.473L40.7354 28.7305L30 47L0 0H18.0882L24.273 7.7653Z"
|
||||
fill="#2E97D1"
|
||||
/>
|
||||
<path
|
||||
d="M43.645 9.14625C42.6195 7.84489 40.7332 7.62124 39.4319 8.64673L30.4992 15.6858L28.646 13.3226C27.6235 12.0188 25.7378 11.7907 24.434 12.8132C23.1302 13.8356 22.9022 15.7213 23.9246 17.0251L29.4784 24.1071L29.4954 24.0939L29.506 24.1074L43.1455 13.3594C44.4468 12.3339 44.6705 10.4476 43.645 9.14625Z"
|
||||
fill="#2E97D1"
|
||||
/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 95 26" style={{ height: "1.5rem" }}>
|
||||
<path
|
||||
d="M2.08 18.8C1.89333 18.8 1.72 18.7667 1.56 18.7C1.4 18.6333 1.26 18.54 1.14 18.42C1.03333 18.3 0.96 18.16 0.92 18C0.88 17.8267 0.886667 17.6467 0.94 17.46L2.28 7.48H14.38L14.18 9.4H5.74L4.78 16.88H12.26L13.22 16.12L12.84 18.8H2.08ZM18.4758 18.8L16.3558 8.04L15.9758 7.48H20.0158L21.5358 15.34L25.3758 7.48H29.2158L23.8558 18.8H18.4758ZM44.1595 7.48L43.3995 13.62C43.2929 14.0067 43.0795 14.3267 42.7595 14.58C42.4529 14.8333 42.1529 14.96 41.8595 14.96H35.1395L34.5595 18.8H30.9195L32.2595 8.04L31.6795 7.48H44.1595ZM35.9195 9.4L35.5195 13.04H39.5595L40.1395 9.4H35.9195ZM51.3409 7.48L49.6009 18.8H45.9609L47.3009 8.24L46.7209 7.48H51.3409ZM58.4238 7.48L57.0838 16.88H64.5638L64.3838 18.8H54.4038C54.0171 18.8 53.7238 18.6733 53.5238 18.42C53.3371 18.1667 53.2438 17.8467 53.2438 17.46L54.4038 8.24L53.8238 7.48H58.4238ZM78.1963 7.48C78.3829 7.48 78.5563 7.51333 78.7163 7.58C78.8763 7.64667 79.0096 7.74 79.1163 7.86C79.2363 7.98 79.3163 8.12 79.3563 8.28C79.3963 8.44 79.3896 8.62 79.3363 8.82L77.9963 18.8H67.2363C67.0496 18.8 66.8763 18.7667 66.7163 18.7C66.5563 18.6333 66.4163 18.54 66.2963 18.42C66.1896 18.3 66.1163 18.16 66.0763 18C66.0363 17.8267 66.0429 17.6467 66.0963 17.46L67.4363 7.48H78.1963ZM70.8963 9.4L69.9363 16.88H74.3563L75.4963 9.4H70.8963ZM84.0534 18.8L85.3934 9.4H81.1734L81.3734 7.48H93.6534L93.2734 9.4H89.2334L87.8934 18.8H84.0534Z"
|
||||
fill="#2E97D1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
.paper {
|
||||
margin: 0 auto 1rem auto;
|
||||
background: #fff;
|
||||
|
||||
&.a4 {
|
||||
padding: 4rem;
|
||||
width: 794px;
|
||||
height: 1123px;
|
||||
}
|
||||
|
||||
>main {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import "./index.less";
|
||||
import { PaperHead } from "./Head";
|
||||
import { PaperFoot, PaperFootProps } from "./Foot";
|
||||
|
||||
interface PaperProps {
|
||||
children?: JSX.Element | any;
|
||||
hiddenHeadFoot?: boolean;
|
||||
}
|
||||
|
||||
type PaperFootPageProps = Pick<
|
||||
PaperFootProps,
|
||||
"date" | "pageNum" | "totalPageNum"
|
||||
>;
|
||||
|
||||
export const Paper = (props: PaperProps & PaperFootPageProps) => {
|
||||
return (
|
||||
<div className="paper a4">
|
||||
<main>
|
||||
{!props.hiddenHeadFoot && <PaperHead />}
|
||||
{props.children}
|
||||
{!props.hiddenHeadFoot && (
|
||||
<PaperFoot
|
||||
date={props.date}
|
||||
pageNum={props.pageNum}
|
||||
totalPageNum={props.totalPageNum}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
p {
|
||||
margin: 0;
|
||||
line-height: 1.29rem;
|
||||
font-weight: 400;
|
||||
text-align: justify;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import './index.less'
|
||||
|
||||
interface ParagraphProps {
|
||||
text: string
|
||||
children?: JSX.Element
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const Paragraph = (props: ParagraphProps) => {
|
||||
return <p style={{ ...props.style }}>{props.text}</p>
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
interface ProgressBarProps {
|
||||
children?: JSX.Element;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
completed: number;
|
||||
}
|
||||
|
||||
export const ProgressBar = (props: ProgressBarProps) => {
|
||||
const fixed = props.completed * 100;
|
||||
|
||||
const containerStyles: React.CSSProperties = {
|
||||
position: "fixed",
|
||||
height: 5,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
backgroundColor: "#e0e0de",
|
||||
};
|
||||
|
||||
const fillerStyles: React.CSSProperties = {
|
||||
height: "100%",
|
||||
width: `${props.completed * 100}%`,
|
||||
backgroundColor: props?.bgColor ?? "red",
|
||||
borderRadius: "inherit",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
const labelStyles: React.CSSProperties = {
|
||||
display: "inline-block",
|
||||
transform: "translateY(2px)",
|
||||
fontSize: "12px",
|
||||
color: props.color ?? "red",
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={fillerStyles}>
|
||||
<span style={labelStyles}>{`${fixed.toFixed(2)}%`}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
.ref-literature {
|
||||
h4 {
|
||||
margin: 0;
|
||||
padding-bottom: 0.43rem;
|
||||
color: var(--color-primary);
|
||||
font-size: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
padding-bottom: 0.27rem;
|
||||
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import "./index.less";
|
||||
|
||||
interface ReferenceProps {
|
||||
children?: JSX.Element;
|
||||
literature: string[];
|
||||
style?: React.CSSProperties;
|
||||
titleVisible?: boolean;
|
||||
}
|
||||
|
||||
export const Reference = (props: ReferenceProps) => {
|
||||
const { titleVisible = false } = props;
|
||||
return (
|
||||
<div className="ref-literature" style={props.style}>
|
||||
{titleVisible && <h4>参考文献:</h4>}
|
||||
{props.literature.map((t, i) => (
|
||||
<p key={i}>{t}</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
.table-cell {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
&.border-outer {
|
||||
border: 1px solid var(--color-table-border);
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
padding: 0 2rem;
|
||||
|
||||
&:nth-of-type(2n+1) {
|
||||
background: var(--color-bg-primary-2);
|
||||
}
|
||||
|
||||
td {
|
||||
padding-left: 2rem;
|
||||
height: 1.29rem;
|
||||
|
||||
&.border-vertical {
|
||||
border-right: 1px solid var(--color-table-border);
|
||||
|
||||
&:first-of-type {
|
||||
border-left: 1px solid var(--color-table-border);
|
||||
}
|
||||
}
|
||||
|
||||
&.border-horizon {
|
||||
border-bottom: 1px solid var(--color-table-border);
|
||||
|
||||
&:first-of-type {
|
||||
border-top: 1px solid var(--color-table-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import './index.less'
|
||||
|
||||
export type TableDataCell = {
|
||||
prop: string
|
||||
content: string
|
||||
colSpan?: number
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
export type TableData = TableDataCell[][]
|
||||
|
||||
interface TableProps {
|
||||
children?: JSX.Element
|
||||
data: TableData
|
||||
style?: React.CSSProperties
|
||||
border?: { vertical?: boolean; horizon?: boolean; outer?: boolean }
|
||||
}
|
||||
|
||||
export const Table = (props: TableProps) => {
|
||||
return (
|
||||
<table
|
||||
style={{ ...props.style }}
|
||||
className={`table-cell ${props.border?.outer ? 'border-outer' : ''}`}
|
||||
>
|
||||
<tbody>
|
||||
{props.data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, cellIndex: number) => {
|
||||
if (typeof cell === 'object') {
|
||||
return (
|
||||
<td
|
||||
key={cellIndex}
|
||||
className={[
|
||||
props.border?.vertical && 'border-vertical',
|
||||
props.border?.horizon && 'border-horizon'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
colSpan={cell.colSpan}
|
||||
>
|
||||
<span>{cell.prop}:</span>
|
||||
<span
|
||||
style={{ paddingLeft: '.7rem' }}
|
||||
className={cell.highlight ? 'highlight' : ''}
|
||||
>
|
||||
{cell.content}
|
||||
</span>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
return <td key={cellIndex}>{cell}</td>
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
48
apps/aorta/src/modules/Report/Full/index.less
Normal file
48
apps/aorta/src/modules/Report/Full/index.less
Normal file
|
@ -0,0 +1,48 @@
|
|||
.report-full-preview {
|
||||
padding: 24px 0;
|
||||
background: #f1f1f1;
|
||||
--color-primary: #2A5DC0;
|
||||
--color-bg-primary-2: #e9eff4;
|
||||
--color-bg-primary: #2E97D1;
|
||||
--color-table-border: #AAC4D8;
|
||||
--color-danger: #F33939;
|
||||
|
||||
|
||||
.action-bar {
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
width: 100px;
|
||||
background: #fff;
|
||||
border: 3px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: calc((100% - 794px) / 2 - 100px - 10px);
|
||||
}
|
||||
|
||||
.risk-module {
|
||||
section {
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--color-bg-primary);
|
||||
padding-bottom: 0.86rem;
|
||||
|
||||
span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2.86rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
132
apps/aorta/src/modules/Report/Full/index.tsx
Normal file
132
apps/aorta/src/modules/Report/Full/index.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Paper } from "./components/Paper";
|
||||
import { Aorta } from "./pages/Aorta";
|
||||
import { Catalogue } from "./pages/Catalogue";
|
||||
import { PatientProfile } from "./pages/PatientProfile";
|
||||
import { ProjectionAngle } from "./pages/ProjectionAngle";
|
||||
import { dom2PDF } from "@tavi/util";
|
||||
import { LVOT } from "./pages/LVOT";
|
||||
import { SOV } from "./pages/SOV";
|
||||
import { STJ } from "./pages/STJ";
|
||||
import { AscAorta } from "./pages/AscAorta";
|
||||
import { AortaValve } from "./pages/AortaValve";
|
||||
import { Calcify } from "./pages/Calcify";
|
||||
import { Slice } from "./pages/Slice";
|
||||
import { RingShape } from "./pages/RingShape";
|
||||
import { Coronary } from "./pages/Coronary";
|
||||
import { RightCoronary } from "./pages/RightCoronary";
|
||||
import { LeftCoronarySinus } from "./pages/LeftCoronarySinus";
|
||||
import { RightCoronarySinus } from "./pages/RightCoronarySinus";
|
||||
import { NoCoronarySinus } from "./pages/NoCoronarySinus";
|
||||
import { ProjectionAngleP2 } from "./pages/ProjectionAngle-P2";
|
||||
import { SuicideLeft } from "./pages/SuicideLeft";
|
||||
import "./index.less";
|
||||
import { ProgressBar } from "./components/ProgressBar";
|
||||
import axios from "axios";
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
interface DoctorProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const ReportFullVersion = (props: DoctorProps) => {
|
||||
const [date, totalPageNum] = ["2023-06-14", 21];
|
||||
const [completed, setCompleted] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/aorta/report/root").then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 下载pdf
|
||||
*/
|
||||
const downloadHandler = () => {
|
||||
const elements = Array.from(
|
||||
document.querySelectorAll(".a4")
|
||||
) as HTMLElement[];
|
||||
dom2PDF({
|
||||
elements,
|
||||
scale: 3,
|
||||
onProgress: (currentPage, totalPage) => {
|
||||
setCompleted(currentPage / totalPage);
|
||||
},
|
||||
}).then((pdf) => pdf.save("test.pdf"));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="report-full-preview" style={{ padding: "24px 0" }}>
|
||||
{completed > 0 && (
|
||||
<ProgressBar
|
||||
completed={completed}
|
||||
bgColor="var(--color-primary)"
|
||||
color="var(--color-primary)"
|
||||
/>
|
||||
)}
|
||||
<div className="action-bar">
|
||||
<button onClick={downloadHandler} className="download-pdf">
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
<Paper date={date}>
|
||||
<PatientProfile />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={1} totalPageNum={totalPageNum}>
|
||||
<Catalogue />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={2} totalPageNum={totalPageNum}>
|
||||
<Aorta />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={3} totalPageNum={totalPageNum}>
|
||||
<LVOT />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={4} totalPageNum={totalPageNum}>
|
||||
<SOV />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={5} totalPageNum={totalPageNum}>
|
||||
<STJ />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={6} totalPageNum={totalPageNum}>
|
||||
<AscAorta />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={7} totalPageNum={totalPageNum}>
|
||||
<AortaValve />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={8} totalPageNum={totalPageNum}>
|
||||
<Calcify />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={9} totalPageNum={totalPageNum}>
|
||||
<Slice />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={10} totalPageNum={totalPageNum}>
|
||||
<RingShape />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={11} totalPageNum={totalPageNum}>
|
||||
<Coronary />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={12} totalPageNum={totalPageNum}>
|
||||
<RightCoronary />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={13} totalPageNum={totalPageNum}>
|
||||
<LeftCoronarySinus />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={14} totalPageNum={totalPageNum}>
|
||||
<RightCoronarySinus />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={15} totalPageNum={totalPageNum}>
|
||||
<NoCoronarySinus />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={16} totalPageNum={totalPageNum}>
|
||||
<ProjectionAngle />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={17} totalPageNum={totalPageNum}>
|
||||
<ProjectionAngleP2 />
|
||||
</Paper>
|
||||
<Paper date={date} pageNum={18} totalPageNum={totalPageNum}>
|
||||
<SuicideLeft />
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
46
apps/aorta/src/modules/Report/Full/pages/Aorta/index.tsx
Normal file
46
apps/aorta/src/modules/Report/Full/pages/Aorta/index.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface AortaProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [
|
||||
[
|
||||
{ prop: "周长", content: "1mm" },
|
||||
{ prop: "周长导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "面积", content: "1mm²" },
|
||||
{ prop: "面积导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "长径", content: "1mm" },
|
||||
{ prop: "短径", content: "1mm" },
|
||||
],
|
||||
[{ prop: "平均径", content: "1mm", colSpan: 1 }],
|
||||
];
|
||||
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const Aorta = (props: AortaProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="主动脉根部的大小和形态"
|
||||
level={1}
|
||||
style={{ marginBottom: "2.36rem" }}
|
||||
/>
|
||||
<Heading title="主动脉瓣环 Annulus" style={{ marginBottom: "1.43rem" }} />
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem src={src} title={"主动脉瓣环 Annulus"} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
|
||||
interface AortaValveProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const AortaValve = (props: AortaValveProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="主动脉瓣的形态和功能"
|
||||
level={1}
|
||||
style={{ marginBottom: "2.36rem" }}
|
||||
/>
|
||||
<Heading title="瓣叶类型" />
|
||||
<p style={{ marginTop: 0, marginBottom: "2.86rem" }}>三叶瓣</p>
|
||||
<ImageItem src={src} />
|
||||
</div>
|
||||
);
|
||||
};
|
46
apps/aorta/src/modules/Report/Full/pages/AscAorta/index.tsx
Normal file
46
apps/aorta/src/modules/Report/Full/pages/AscAorta/index.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface AscAortaProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [
|
||||
[
|
||||
{ prop: "周长", content: "1mm" },
|
||||
{ prop: "周长导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "面积", content: "1mm²" },
|
||||
{ prop: "面积导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "长径", content: "1mm" },
|
||||
{ prop: "短径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "平均径", content: "1mm" },
|
||||
{ prop: "窦管交界平面高度", content: "22.50mm" },
|
||||
],
|
||||
];
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const AscAorta = (props: AscAortaProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="升主动脉 Ascending Aorta"
|
||||
style={{ marginBottom: "1.43rem" }}
|
||||
/>
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem src={src} title={"升主动脉 Ascending Aorta"} />
|
||||
</div>
|
||||
);
|
||||
};
|
56
apps/aorta/src/modules/Report/Full/pages/Calcify/index.tsx
Normal file
56
apps/aorta/src/modules/Report/Full/pages/Calcify/index.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* 钙化形态
|
||||
* PageNumber = 9
|
||||
*/
|
||||
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
import { Reference } from "../../components/Reference";
|
||||
|
||||
interface CalcifyProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [
|
||||
[
|
||||
{ prop: "疑似钙化区域体积", content: "1351.53mmm³", highlight: true },
|
||||
{ prop: "Hu850", content: "72.00mm³" },
|
||||
],
|
||||
];
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const Calcify = (props: CalcifyProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading title="钙化形态" style={{ marginBottom: "2.36rem" }} />
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<div className="risk-module">
|
||||
<section className="valve-ring-risk">
|
||||
<h2 className="risk-evaluate">冠脉阻塞风险程度:重度</h2>
|
||||
<img src="" alt="" />
|
||||
</section>
|
||||
<section>
|
||||
<h3>
|
||||
冠脉阻塞风险定义<span>(可见参考文献)</span>
|
||||
</h3>
|
||||
<p>{"钙化积分800<X<=1000轻度、1000<X<=1500中度、X>1500以上重度。"}</p>
|
||||
</section>
|
||||
</div>
|
||||
<ImageItem src={src} title={"疑似钙化区域"} />
|
||||
<Reference
|
||||
titleVisible
|
||||
style={{ position: "absolute", bottom: "3rem" }}
|
||||
literature={[
|
||||
"Miralem Pasic, Axel Unbehaun, Semih Buz, Thorsten Drews, Roland Hetzer. Annular Rupture During TAVR: Despite It, Dr. Alain Cribier Should Receive a Nobel Prize. JACC: Cardiovascular Interventionsthis link is disabled, 2023, 13(15), 1800-1802.",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
.catalogue {
|
||||
>div {
|
||||
color: var(--color-primary);
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.71rem;
|
||||
border-bottom: 3px solid var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-top: 2rem;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--color-bg-primary-2);
|
||||
border-right: 0.29rem solid var(--color-bg-primary);
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.89rem;
|
||||
width: 1.89rem;
|
||||
background: var(--color-bg-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-left: 0.89rem;
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 1.14rem 0 1.14rem 2.89rem;
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 2.5rem 1fr auto;
|
||||
padding-bottom: 0.7rem;
|
||||
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
>div {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
>span b {
|
||||
font-weight: 400;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
>div {
|
||||
padding: 0 0.86rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
em {
|
||||
display: inline-block;
|
||||
border-bottom: 2px dotted #000;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-number {
|
||||
display: inline-block;
|
||||
padding-right: 0.29rem;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
apps/aorta/src/modules/Report/Full/pages/Catalogue/index.tsx
Normal file
136
apps/aorta/src/modules/Report/Full/pages/Catalogue/index.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import "./index.less";
|
||||
import { Section } from "./interface";
|
||||
|
||||
interface CatalogueProps {
|
||||
dataSource?: [];
|
||||
}
|
||||
|
||||
const mockData: Section[] = [
|
||||
{
|
||||
title: "主动脉根部的大小和形态",
|
||||
subsections: [
|
||||
{
|
||||
title: "主动脉瓣环 Annulus",
|
||||
pageNumber: 2,
|
||||
},
|
||||
{
|
||||
title: "左室流出道 LVOT",
|
||||
pageNumber: 3,
|
||||
},
|
||||
{
|
||||
title: "瓦氏窦 SOV",
|
||||
pageNumber: 4,
|
||||
},
|
||||
{
|
||||
title: "窦管交界 STJ",
|
||||
pageNumber: 5,
|
||||
},
|
||||
{
|
||||
title: "升主动脉 Ascending Aorta",
|
||||
pageNumber: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "主动脉瓣的形态和功能",
|
||||
subsections: [
|
||||
{
|
||||
title: "瓣叶类型",
|
||||
pageNumber: 7,
|
||||
},
|
||||
{
|
||||
title: "钙化形态",
|
||||
pageNumber: 8,
|
||||
},
|
||||
{
|
||||
title: "分割形态",
|
||||
pageNumber: 9,
|
||||
},
|
||||
{
|
||||
title: "环上形态",
|
||||
pageNumber: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "冠状动脉和临近结构",
|
||||
subsections: [
|
||||
{
|
||||
title: "左冠开口高度",
|
||||
pageNumber: 11,
|
||||
},
|
||||
{
|
||||
title: "右冠开口高度",
|
||||
pageNumber: 12,
|
||||
},
|
||||
{
|
||||
title: "左冠窦瓣叶长度",
|
||||
forRef: true,
|
||||
pageNumber: 13,
|
||||
},
|
||||
{
|
||||
title: "右冠窦瓣叶长度",
|
||||
forRef: true,
|
||||
pageNumber: 14,
|
||||
},
|
||||
{
|
||||
title: "无冠窦瓣叶长度",
|
||||
forRef: true,
|
||||
pageNumber: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "投照角度",
|
||||
subsections: [
|
||||
{
|
||||
title: "建议投照角度",
|
||||
pageNumber: 16,
|
||||
},
|
||||
{
|
||||
title: "三维重建效果",
|
||||
pageNumber: 17,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Catalogue = (props: CatalogueProps) => {
|
||||
return (
|
||||
<section className="catalogue">
|
||||
<Heading title="目录" level={1} />
|
||||
<ul>
|
||||
{mockData.map((section, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<header>
|
||||
<span>{i + 1}</span>
|
||||
<p>{section.title}</p>
|
||||
</header>
|
||||
<ol>
|
||||
{section.subsections?.map((sub, i_sub) => (
|
||||
<li key={`${i}-${i_sub}`}>
|
||||
<span>
|
||||
{i + 1}-{i_sub + 1}
|
||||
</span>
|
||||
<div>
|
||||
<span>
|
||||
{sub.title}
|
||||
{sub.forRef && <b>(仅供参考)</b>}
|
||||
</span>
|
||||
<div>
|
||||
<em></em>
|
||||
</div>
|
||||
</div>
|
||||
<span className="page-number">{sub.pageNumber}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
export interface Section {
|
||||
title: string;
|
||||
subsections?: Subsection[];
|
||||
}
|
||||
|
||||
export interface Subsection {
|
||||
title: string;
|
||||
/**
|
||||
* 是否在title后面加上仅供参考小字
|
||||
*/
|
||||
forRef?: boolean;
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
pageNumber: number;
|
||||
}
|
||||
|
||||
export interface Contents {
|
||||
sections: Section[];
|
||||
}
|
55
apps/aorta/src/modules/Report/Full/pages/Coronary/index.tsx
Normal file
55
apps/aorta/src/modules/Report/Full/pages/Coronary/index.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Reference } from "../../components/Reference";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface CoronaryProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [
|
||||
[{ prop: "左冠开口高度", content: "9.67mm", colSpan: 2, highlight: true }],
|
||||
];
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const Coronary = (props: CoronaryProps) => {
|
||||
return (
|
||||
<div className="coronary">
|
||||
<Heading
|
||||
title="冠状动脉和临近结构"
|
||||
level={1}
|
||||
style={{ marginBottom: "2.36rem" }}
|
||||
/>
|
||||
<Heading title="左冠开口高度" style={{ marginBottom: "1.43rem" }} />
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<div className="risk-module">
|
||||
<section>
|
||||
<h2>冠脉阻塞风险程度:重度</h2>
|
||||
<img src="" alt="" />
|
||||
</section>
|
||||
<section>
|
||||
<h3>
|
||||
冠脉阻塞风险定义<span>(可见参考文献)</span>
|
||||
</h3>
|
||||
<p>
|
||||
窦高30mm以下轻度,窦高30mm以下(开口高度12以下)中度,窦高30mm以下(开口高度12以下、瓣叶长度12以上)重度。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<ImageItem src={src} title={"左冠开口高度"} />
|
||||
<Reference
|
||||
titleVisible
|
||||
style={{ position: "absolute", bottom: "3rem" }}
|
||||
literature={[
|
||||
"【1】Oluwaseun A. Akinseye, Sunil K. Jha, Uzoma N. Ibebuogu. Clinical Outcomes of Coro- nary Occlusion Following Transcatheter Aortic Valve Replacement: A Systematic Review.",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
40
apps/aorta/src/modules/Report/Full/pages/LVOT/index.tsx
Normal file
40
apps/aorta/src/modules/Report/Full/pages/LVOT/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface LVOTProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [
|
||||
[
|
||||
{ prop: "周长", content: "1mm" },
|
||||
{ prop: "周长导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "面积", content: "1mm²" },
|
||||
{ prop: "面积导出径", content: "1mm" },
|
||||
],
|
||||
[
|
||||
{ prop: "长径", content: "1mm" },
|
||||
{ prop: "短径", content: "1mm" },
|
||||
],
|
||||
[{ prop: "平均径", content: "1mm", colSpan: 1 }],
|
||||
];
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const LVOT = (props: LVOTProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading title="左室流出道 LVOT" style={{ marginBottom: "1.43rem" }} />
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem src={src} title={"左室流出道 LVOT"} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* 左冠窦瓣叶长度
|
||||
*/
|
||||
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Reference } from "../../components/Reference";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface LeftCoronarySinusProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
const data = [
|
||||
[{ prop: "左冠开口高度", content: "9.67mm", colSpan: 2, highlight: true }],
|
||||
];
|
||||
|
||||
export const LeftCoronarySinus = (props: LeftCoronarySinusProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="左冠窦瓣叶长度"
|
||||
forRef
|
||||
style={{ marginBottom: "1.43rem" }}
|
||||
/>
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<div className="risk-module">
|
||||
<section>
|
||||
<h2>冠脉阻塞风险程度:重度</h2>
|
||||
<img src="" alt="" />
|
||||
</section>
|
||||
<section>
|
||||
<h3>
|
||||
冠脉阻塞风险定义<span>(可见参考文献)</span>
|
||||
</h3>
|
||||
<p>
|
||||
窦高30mm以下轻度,窦高30mm以下(开口高度12以下)中度,窦高30mm以下(开口高度12以下、瓣叶长度12以上)重度。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<ImageItem
|
||||
title="左冠窦瓣叶长度"
|
||||
src={src}
|
||||
imgStyle={{ height: "6.43rem" }}
|
||||
/>
|
||||
<Reference
|
||||
style={{ position: "absolute", bottom: "3rem" }}
|
||||
literature={[
|
||||
"【1】Oluwaseun A. Akinseye, Sunil K. Jha, Uzoma N. Ibebuogu. Clinical Outcomes of Coro- nary Occlusion Following Transcatheter Aortic Valve Replacement: A Systematic Review. Cardiovasc Revasc Med. 2018 Mar;19(2):229-236. doi: 10.1016/j.carrev.2017.09.006. doi:10.1016/j.carrev.2017.09.006",
|
||||
"【2】Roberto Valvo, Giuliano Costa, Marco Barbanti. How to Avoid Coronary Occlusion During TAVR Valve-in-Valve Procedures. Front. Cardiovasc. Med. 6:168.",
|
||||
]}
|
||||
titleVisible
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 左冠窦瓣叶长度
|
||||
*/
|
||||
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface NoCoronarySinusProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
const data = [[{ prop: "无冠窦瓣叶长度", content: "9.67mm", colSpan: 2 }]];
|
||||
|
||||
export const NoCoronarySinus = (props: NoCoronarySinusProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="无冠窦瓣叶长度"
|
||||
forRef
|
||||
style={{ marginBottom: "1.43rem" }}
|
||||
/>
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem
|
||||
title="无冠窦瓣叶长度"
|
||||
src={src}
|
||||
imgStyle={{ height: "6.43rem" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
.patient-info {
|
||||
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
border: 1px solid #AAC4D8;
|
||||
|
||||
.item {
|
||||
padding: 10px;
|
||||
|
||||
&.full-width {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
em {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
min-width: 4.6rem;
|
||||
}
|
||||
|
||||
&:nth-of-type(1),
|
||||
&:nth-of-type(2),
|
||||
&:nth-of-type(5),
|
||||
&:nth-of-type(6) {
|
||||
background: #E9EFF4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide-list {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-bottom: 4.43rem;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 1.43rem;
|
||||
|
||||
h3 {
|
||||
position: relative;
|
||||
padding-left: 1rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--color-bg-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
padding-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { Paragraph } from "../../components/Paragraph";
|
||||
import { Reference } from "../../components/Reference";
|
||||
import "./index.less";
|
||||
|
||||
interface PatientProfileProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
prop: "姓名",
|
||||
value: "刘伟",
|
||||
},
|
||||
{
|
||||
prop: "性别",
|
||||
value: "男",
|
||||
},
|
||||
{
|
||||
prop: "年龄",
|
||||
value: "68",
|
||||
},
|
||||
{
|
||||
prop: "医院",
|
||||
value: "阜外医院",
|
||||
},
|
||||
{
|
||||
prop: "报告日期",
|
||||
value: "2023-06-14",
|
||||
},
|
||||
{
|
||||
prop: "扫描日期",
|
||||
value: "2023-06-14",
|
||||
},
|
||||
{
|
||||
prop: "序列名称",
|
||||
value: "DS_CorCTA 0.75 I26f 3BestSyst 47 %",
|
||||
block: true,
|
||||
},
|
||||
];
|
||||
|
||||
const guideData = [
|
||||
{
|
||||
title: "主动脉根部的大小和形态",
|
||||
content: "对冠状动脉解剖结构,以及TAVI和其他经导管手术的适用性进行评估诊断",
|
||||
},
|
||||
{
|
||||
title: "主动脉瓣的形态和功能",
|
||||
content:
|
||||
"对主动脉瓣膜、瓣叶、钙化,以及先天性畸形而导致瓣叶无法正常开启进行评估诊断",
|
||||
},
|
||||
{
|
||||
title: "冠状动脉和临近结构",
|
||||
content: "对TAVR手术后可能产生的并发症进行风险评估",
|
||||
},
|
||||
{
|
||||
title: "投照角度",
|
||||
content: "对主动脉根部进行三维模型重建,并对手术过程进行模拟",
|
||||
},
|
||||
];
|
||||
|
||||
export const PatientProfile = (props: PatientProfileProps) => {
|
||||
return (
|
||||
<div className="patient-info">
|
||||
<Heading title="检测内容" />
|
||||
<Paragraph
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
text="CVPILOT是一款基于主动脉CT影像的心脏瓣膜疾病手术分析辅助决策系统。在术前全自动完成TAVR手术的结构标记、手术规划及风险判断,保障介入瓣膜的高精准植入。"
|
||||
/>
|
||||
<Heading title="患者基本信息" />
|
||||
<div className="table" style={{ marginBottom: "2.86rem" }}>
|
||||
{tableData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={["item", item.block && "full-width"]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<em>{item.prop}</em>
|
||||
<span>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Heading title="指南分析" />
|
||||
<ul className="guide-list">
|
||||
{guideData.map((guide, i) => (
|
||||
<li key={i}>
|
||||
<h3>{guide.title}</h3>
|
||||
<p>{guide.content}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Reference
|
||||
titleVisible
|
||||
style={{ position: "absolute", bottom: "3rem" }}
|
||||
literature={[
|
||||
"Catherine M. Otto, Rick A. Nishimura, Robert O. Bonow, Blase A. Carabello, John P. Erwin III,Federico Gentile, Hani Jneid, Eric V. Krieger, Michael Mack, Christopher McLeod, Patrick T. O’Gara, Vera H. Rigolin, Thoralf M. Sundt III, Annemarie Thompson, Christopher Toly. 2020 ACC/AHA Guideline for the Management of Patients With Valvular Heart Disease.Circulation. 2021;143:e35–e71. DOI: 10.1161/CIR.0000000000000932.",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
.project-angle-p2 {
|
||||
.image-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 2.43rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { ImageItem } from "../../components/ImageItem";
|
||||
import "./index.less";
|
||||
import { Reference } from "../../components/Reference";
|
||||
|
||||
interface ProjectionAngleP2Props {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const ProjectionAngleP2 = (props: ProjectionAngleP2Props) => {
|
||||
return (
|
||||
<div className="project-angle-p2">
|
||||
<div className="image-group">
|
||||
<ImageItem title="右窦居中" src={src} />
|
||||
<ImageItem title="左右窦重合(Cusp-Overlap)" src={src} />
|
||||
</div>
|
||||
<Reference
|
||||
titleVisible
|
||||
style={{ position: "absolute", bottom: "3rem" }}
|
||||
literature={[
|
||||
"【1】Kaneko U, Hachinohe D, Kobayashi K, Shitan H, Mitsube K, Furugen A, Kawamura T, Koshima R, Fujita T. Evolut Self-Expanding Transcatheter Aortic Valve Replacement in Patients with Extremely Horizontal Aorta (Aortic Root Angle ≥ 70°). Int Heart J. 2020 Sep 29;61(5):1059-1069. doi: 10.1536/ihj.20-120. Epub 2020 Sep 12.",
|
||||
"【2】Espinoza Rueda MA, Muratalla González R, García García JF, Morales Portano JD, Alcántara Meléndez MA, Jiménez Valverde AS, Rivas Gálvez RE, Campos Delgadillo JL, González CL, Gayosso Ortiz JR, Merino Rajme JA. Description of the Step-by-Step Technique With Snare Catheter for TAVR in Horizontal Aorta. JACC Case Rep. 2021 Dec 1;3(17):1811-1815. doi: 10.1016/j.jaccas.2021.09.006.",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
.project-angle {
|
||||
.annotation {
|
||||
padding-top: 0.57rem;
|
||||
color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin-right: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
>em {
|
||||
display: inline-block;
|
||||
width: 0.57rem;
|
||||
height: 0.57rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 0.29rem;
|
||||
color: var(--color-bg-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 2.43rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { Table, TableData } from "../../components/Table";
|
||||
import "./index.less";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
interface ProjectionAngleProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
const tableData: TableData = [
|
||||
[
|
||||
{
|
||||
prop: "瓣环平面与水平面夹角",
|
||||
content: "66deg",
|
||||
highlight: true,
|
||||
colSpan: 5,
|
||||
},
|
||||
],
|
||||
[
|
||||
{ prop: "右窦居中", content: "" },
|
||||
{ prop: "LAO", content: "15deg" },
|
||||
{ prop: "CAU", content: "25deg" },
|
||||
],
|
||||
[
|
||||
{ prop: "左右窦重合(Cusp-Overlap)", content: "" },
|
||||
{ prop: "LAO", content: "15deg" },
|
||||
{ prop: "CAU", content: "25deg" },
|
||||
],
|
||||
[
|
||||
{ prop: "无冠窦平行法", content: "" },
|
||||
{ prop: "LAO", content: "15deg" },
|
||||
{ prop: "CAU", content: "25deg" },
|
||||
],
|
||||
[
|
||||
{ prop: "右无重合", content: "" },
|
||||
{ prop: "LAO", content: "15deg" },
|
||||
{ prop: "CAU", content: "25deg" },
|
||||
],
|
||||
];
|
||||
|
||||
const dots = [
|
||||
{
|
||||
color: "#BA503D",
|
||||
alias: "左冠窦窦底",
|
||||
},
|
||||
{
|
||||
color: "#E7B551",
|
||||
alias: "右冠窦窦底",
|
||||
},
|
||||
{
|
||||
color: "#4B9979",
|
||||
alias: "无冠窦窦底",
|
||||
},
|
||||
];
|
||||
|
||||
export const ProjectionAngle = (props: ProjectionAngleProps) => {
|
||||
return (
|
||||
<div className="project-angle">
|
||||
<Heading title="投照角度" level={1} style={{ marginBottom: "2.36rem" }} />
|
||||
<Heading title="建议投照角度" style={{ marginBottom: "1.43rem" }} />
|
||||
<Table data={tableData} border={{ outer: true }} />
|
||||
<p className="annotation">
|
||||
注:deg即Degree的缩写,表示角度制。例:15deg=15°
|
||||
</p>
|
||||
<div className="risk-module">
|
||||
<section>
|
||||
<h2 className="risk-evaluate">心脏横位风险程度:中度</h2>
|
||||
<img src="" alt="" />
|
||||
</section>
|
||||
<section>
|
||||
<h3>
|
||||
心脏横位风险定义<span>(可见参考文献)</span>
|
||||
</h3>
|
||||
<p>
|
||||
瓣环平面与水平面夹角,48deg-58deg轻度、59deg-70deg中度,70deg以上重度。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<ul className="dots" style={{ marginBottom: "1.43rem" }}>
|
||||
{dots.map((i, index) => (
|
||||
<li key={index}>
|
||||
<em style={{ background: i.color }}></em>
|
||||
<span>{i.alias}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="image-group">
|
||||
<ImageItem title="右窦居中" src={src} />
|
||||
<ImageItem title="左右窦重合(Cusp-Overlap)" src={src} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface RightCoronaryProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const data = [[{ prop: "右冠开口高度", content: "1mm", colSpan: 2 }]];
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
export const RightCoronary = (props: RightCoronaryProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading title="右冠开口高度" style={{ marginBottom: "1.43rem" }} />
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem src={src} title="左冠开口高度" />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* 左冠窦瓣叶长度
|
||||
*/
|
||||
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { ImageItem } from "../../components/ImageItem";
|
||||
import { Reference } from "../../components/Reference";
|
||||
import { Table } from "../../components/Table";
|
||||
|
||||
interface RightCoronarySinusProps {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const src =
|
||||
"https://1500021350.vod2.myqcloud.com/cbafb25avodsh1500021350/e7f802d33270835009150070462/hgdT5ulA9RwA.png";
|
||||
|
||||
const data = [[{ prop: "右冠开口高度", content: "9.67mm", colSpan: 2 }]];
|
||||
|
||||
export const RightCoronarySinus = (props: RightCoronarySinusProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Heading
|
||||
title="右冠窦瓣叶长度"
|
||||
forRef
|
||||
style={{ marginBottom: "1.43rem" }}
|
||||
/>
|
||||
<Table
|
||||
data={data}
|
||||
border={{ vertical: true, outer: true }}
|
||||
style={{ marginBottom: "2.86rem" }}
|
||||
/>
|
||||
<ImageItem
|
||||
title="左冠窦瓣叶长度"
|
||||
src={src}
|
||||
imgStyle={{ height: "6.43rem" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user