first commit

This commit is contained in:
root 2023-08-27 14:37:59 +08:00
commit 9578a29130
407 changed files with 27471 additions and 0 deletions

26
.changeset/README.md Normal file
View 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
View 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": []
}

View File

@ -0,0 +1,5 @@
---
"@tavi/i18n": patch
---
publishi test

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/node_modules
**/dist
**/.DS_Store
**/pg_data
**/orthanc_db

2
.npmrc Normal file
View 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
View 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进行使用。一旦你将一个类列为模块的providerNestJS就会负责在需要时创建和销毁这个类的实例。
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
View 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

View 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), // 过滤空值
};

View File

@ -0,0 +1,11 @@
应用层application这一层负责协调领域层和基础设施层实现具体的用例逻辑。
协调领域服务如果有一种业务用例需要多个领域对象或领域服务协作完成UserApplicationService就可以负责调度它们然后完成业务逻辑。比如在创建一个新用户的时候可能需要检查用户名是否已经存在然后再创建新的用户实体这就需要UserApplicationService协调不同的领域服务。
事务控制UserApplicationService也可能负责控制数据库事务。虽然在前端的场景中事务控制可能并不常见但在后端服务中这是非常常见的。比如创建一个新用户可能涉及到在几个数据库表中插入数据这就需要在一个数据库事务中完成。
安全和授权UserApplicationService可能需要检查当前用户是否有权限执行某项操作。比如在更新用户信息的时候可能需要检查当前用户是否有权限更新这个用户的信息。
适配领域层与接口层UserApplicationService也可能负责转换数据格式以便领域层和接口层之间的数据交换。例如将领域实体转换为DTO数据传输对象或者将来自接口层的请求数据转换为领域服务可以处理的格式。
在设计UserApplicationService时需要注意的是业务逻辑应该尽量放在领域层处理应用层更多地是做协调和编排的工作而不是包含业务逻辑。这样可以保证业务逻辑的集中和一致性也使得业务逻辑更易于测试和重用。

View File

@ -0,0 +1,3 @@
import mitt from "mitt";
export const emitter = mitt();

View File

@ -0,0 +1,5 @@
import { Apis } from "@@/infra/api";
export class TrackerRepository {
async report(msg: string) {}
}

View 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;
};
}
}

View 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 };
}
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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. 实例化之后的属性注入,无法被追踪到

View 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);

View 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"),
};

View File

@ -0,0 +1 @@
基础设施层infrastructure这一层包含了所有的基础设施服务例如数据持久化、网络请求等。

1
apps/aorta/env/.env.development vendored Normal file
View File

@ -0,0 +1 @@
REACT_APP_API_URL=https://api-dev.com

1
apps/aorta/env/.env.production vendored Normal file
View File

@ -0,0 +1 @@
REACT_APP_API_URL=https://api-prod.com

0
apps/aorta/env/.env.test vendored Normal file
View File

67
apps/aorta/package.json Normal file
View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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>

View 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"
}
}
}

View 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"
}
}

View 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": "忘记密码"
}
}

View 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": "忘记密码"
}
}

View 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": "" },
},
};

View File

@ -0,0 +1,4 @@
export default {
test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
use: "babel-loader"
}

View 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]', // 文件输出目录和命名
},
}

View 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]', // 文件输出目录和命名
},
}

View File

@ -0,0 +1,9 @@
export default {
// 匹配json文件
test: /\.json$/,
type: "asset/source", // 将json文件视为文件类型
generator: {
// 这里专门针对json文件的处理
filename: 'static/fonts/[name].[contenthash:8][ext]'
}
}

View 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
}
}
}
]
}

View 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;

View 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

View 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;

View 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;

View 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;

View 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
View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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";

View 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>
);
};

View 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>
);
};

View File

@ -0,0 +1,4 @@
import { useContext } from "react";
import { AuthContext } from "../context/auth";
export const useAuth = () => useContext(AuthContext);

View 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;
};

View 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
View 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>
);

View 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>
);
};

View 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>
);
};

View 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%);
}
}
}
}
}
}

View 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>
);
};

View File

@ -0,0 +1,7 @@
interface PatientListProps {
children?: JSX.Element;
}
export const PatientList = (props: PatientListProps) => {
return <div>PatientList</div>;
};

View File

@ -0,0 +1,7 @@
interface PeripheralViewerProps {
children?: JSX.Element;
}
export const PeripheralViewer = (props: PeripheralViewerProps) => {
return <div>PeripheralViewer</div>;
};

View File

@ -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);
}
}
}

View File

@ -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]
}

View File

@ -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);
}
}
}
}

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -0,0 +1,15 @@
.paper {
margin: 0 auto 1rem auto;
background: #fff;
&.a4 {
padding: 4rem;
width: 794px;
height: 1123px;
}
>main {
position: relative;
height: 100%;
}
}

View File

@ -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>
);
};

View File

@ -0,0 +1,6 @@
p {
margin: 0;
line-height: 1.29rem;
font-weight: 400;
text-align: justify;
}

View File

@ -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>
}

View File

@ -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>
);
};

View File

@ -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;
}
}
}

View File

@ -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>
);
};

View File

@ -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;
}
}
}
}

View File

@ -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>
)
}

View 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;
}
}
}
}

View 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>
);
};

View 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>
);
};

View File

@ -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>
);
};

View 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>
);
};

View 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>
);
};

View File

@ -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;
}
}
}
}
}
}

View 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>
);
};

View File

@ -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[];
}

View 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以下1230mm以下1212
</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>
);
};

View 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>
);
};

View File

@ -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以下1230mm以下1212
</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>
);
};

View File

@ -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>
);
};

View File

@ -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;
}
}
}
}

View File

@ -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. OGara, 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:e35e71. DOI: 10.1161/CIR.0000000000000932.",
]}
/>
</div>
);
};

View File

@ -0,0 +1,7 @@
.project-angle-p2 {
.image-group {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 2.43rem;
}
}

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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