chore: replace components with core-ui, upgrade eslint
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
src/components/icon-font/iconfont/iconfont.js
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: require.resolve('@umijs/max/eslint'),
|
|
||||||
rules: {
|
|
||||||
'react/no-unstable-nested-components': 1,
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'no-undef': 'error',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/class-name-casing': 'off'
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
Global: 'readonly',
|
|
||||||
React: 'readonly',
|
|
||||||
JSX: 'readonly'
|
|
||||||
},
|
|
||||||
ignorePatterns: ['public/static/']
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import unusedImports from 'eslint-plugin-unused-imports';
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores([
|
||||||
|
'public/static/',
|
||||||
|
'dist',
|
||||||
|
'src/.umi/',
|
||||||
|
'src/.umi-production/',
|
||||||
|
'src/.umi-test/'
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
prettier
|
||||||
|
],
|
||||||
|
plugins: {
|
||||||
|
react: reactPlugin,
|
||||||
|
import: importPlugin,
|
||||||
|
'unused-imports': unusedImports
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect'
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
node: true,
|
||||||
|
typescript: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
Global: 'readonly',
|
||||||
|
React: 'readonly',
|
||||||
|
JSX: 'readonly'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'react/no-unstable-nested-components': 'warn',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/ban-types': 'off',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'unused-imports/no-unused-vars': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
|
'react-hooks/preserve-manual-memoization': 'off',
|
||||||
|
'react-hooks/set-state-in-effect': 'off',
|
||||||
|
'react-hooks/refs': 'off',
|
||||||
|
'react-hooks/use-memo': 'off',
|
||||||
|
'react-hooks/immutability': 'off',
|
||||||
|
'no-unsafe-optional-chaining': 'off',
|
||||||
|
'no-empty': 'off',
|
||||||
|
'no-constant-condition': 'off',
|
||||||
|
'no-prototype-builtins': 'off',
|
||||||
|
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0, maxBOF: 0 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
+11
-2
@@ -19,6 +19,7 @@
|
|||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
|
"@gpustack/core-ui": "workspace:*",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@@ -96,8 +98,14 @@
|
|||||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.39.4",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
|
"globals": "^17.4.0",
|
||||||
"extract-css-loader": "^0.0.1",
|
"extract-css-loader": "^0.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
@@ -111,6 +119,7 @@
|
|||||||
"prettier-plugin-two-style-order": "^1.0.1",
|
"prettier-plugin-two-style-order": "^1.0.1",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
|
"typescript-eslint": "^8.58.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"worker-loader": "^3.0.8"
|
"worker-loader": "^3.0.8"
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import {
|
|
||||||
CheckCircleFilled,
|
|
||||||
LoadingOutlined,
|
|
||||||
WarningFilled
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
|
||||||
interface AlertInfoProps {
|
|
||||||
type: Global.MessageType;
|
|
||||||
message: React.ReactNode;
|
|
||||||
rows?: number;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
ellipsis?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
contentStyle?: React.CSSProperties;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
maxHeight?: number;
|
|
||||||
overlayScrollerProps?: OverlayScrollerOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => {
|
|
||||||
return {
|
|
||||||
alertBlockInfo: css`
|
|
||||||
padding-block: 6px;
|
|
||||||
padding-inline: 10px 16px;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 32px;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: ${token.borderRadiusLG}px;
|
|
||||||
margin: 0;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
.ant-typography {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.danger {
|
|
||||||
border-color: ${token.colorErrorBorder};
|
|
||||||
background-color: ${token.colorErrorBg};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
border-color: ${token.colorWarningBorder};
|
|
||||||
background-color: ${token.colorWarningBg};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.transition {
|
|
||||||
color: ${token.geekblue7};
|
|
||||||
background: ${token.geekblue1};
|
|
||||||
border-color: ${token.geekblue3};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
border: 1px solid ${token.colorSuccess};
|
|
||||||
color: ${token.colorSuccessText};
|
|
||||||
background: ${token.colorSuccessBg};
|
|
||||||
|
|
||||||
.content.success {
|
|
||||||
font-weight: var(--font-weight-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
height: 32px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0;
|
|
||||||
|
|
||||||
.info-icon {
|
|
||||||
&.danger {
|
|
||||||
color: ${token.colorErrorText};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
color: ${token.colorWarningText};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.transition {
|
|
||||||
color: ${token.geekblue7};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: ${token.colorSuccessText};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const TitleWrapper = styled.div`
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ContentWrapper = styled.div<{ $hasTitle: boolean }>`
|
|
||||||
word-break: break-word;
|
|
||||||
color: ${(props) =>
|
|
||||||
props.$hasTitle
|
|
||||||
? 'var(--ant-color-text-secondary)'
|
|
||||||
: 'var(--ant-color-text)'};
|
|
||||||
font-weight: var(--font-weight-500);
|
|
||||||
white-space: pre-line;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|
||||||
const {
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
rows = 1,
|
|
||||||
ellipsis,
|
|
||||||
style,
|
|
||||||
title,
|
|
||||||
contentStyle,
|
|
||||||
icon,
|
|
||||||
maxHeight = 86,
|
|
||||||
overlayScrollerProps = {}
|
|
||||||
} = props;
|
|
||||||
const { styles } = useStyles();
|
|
||||||
|
|
||||||
const renderIcon = () => {
|
|
||||||
if (type === 'transition') {
|
|
||||||
return <LoadingOutlined />;
|
|
||||||
}
|
|
||||||
if (type === 'success') {
|
|
||||||
return <CheckCircleFilled />;
|
|
||||||
}
|
|
||||||
return <WarningFilled />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{message ? (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.alertBlockInfo, type)}
|
|
||||||
style={{ ...style }}
|
|
||||||
>
|
|
||||||
<Typography.Paragraph
|
|
||||||
ellipsis={
|
|
||||||
ellipsis ?? {
|
|
||||||
rows: rows,
|
|
||||||
tooltip: message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={classNames('title', type)}>
|
|
||||||
<span className={classNames('info-icon', type)}>
|
|
||||||
{icon ?? renderIcon()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{title && (
|
|
||||||
<TitleWrapper className="title-text">{title}</TitleWrapper>
|
|
||||||
)}
|
|
||||||
<OverlayScroller
|
|
||||||
maxHeight={maxHeight}
|
|
||||||
style={{ ...contentStyle }}
|
|
||||||
{...overlayScrollerProps}
|
|
||||||
>
|
|
||||||
<ContentWrapper
|
|
||||||
$hasTitle={!!title}
|
|
||||||
className={classNames('content', type)}
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</ContentWrapper>
|
|
||||||
</OverlayScroller>
|
|
||||||
</Typography.Paragraph>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlertInfo;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { WarningOutlined } from '@ant-design/icons';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface AlertInfoProps {
|
|
||||||
type: 'danger' | 'warning';
|
|
||||||
message: string;
|
|
||||||
rows?: number;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
ellipsis?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|
||||||
const { message, type, rows = 1, ellipsis, style } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{message ? (
|
|
||||||
<Typography.Paragraph
|
|
||||||
type={type}
|
|
||||||
ellipsis={
|
|
||||||
ellipsis !== undefined
|
|
||||||
? ellipsis
|
|
||||||
: {
|
|
||||||
rows: rows,
|
|
||||||
tooltip: message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
fontWeight: 400,
|
|
||||||
whiteSpace: 'pre-line',
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '2px 5px',
|
|
||||||
borderRadius: 'var(--border-radius-base)',
|
|
||||||
margin: 0,
|
|
||||||
backgroundColor: 'var(--ant-color-error-bg)',
|
|
||||||
...style
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WarningOutlined className="m-r-8" />
|
|
||||||
{message}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlertInfo;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.canvas-wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroller-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import useResizeObserver from '@/components/logs-viewer/use-size';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
interface AudioAnimationProps {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
scaleFactor?: number;
|
|
||||||
maxBarCount?: number;
|
|
||||||
amplitude?: number;
|
|
||||||
fixedHeight?: boolean;
|
|
||||||
analyserData: {
|
|
||||||
data: Uint8Array;
|
|
||||||
analyser: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
|
|
||||||
const {
|
|
||||||
scaleFactor = 1.2,
|
|
||||||
maxBarCount = 128,
|
|
||||||
amplitude = 40,
|
|
||||||
maxWidth,
|
|
||||||
fixedHeight = true,
|
|
||||||
analyserData,
|
|
||||||
width: initialWidth,
|
|
||||||
height: initialHeight
|
|
||||||
} = props;
|
|
||||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
||||||
const animationId = React.useRef<number>(0);
|
|
||||||
const isScaled = React.useRef<boolean>(false);
|
|
||||||
const oscillationOffset = React.useRef(0);
|
|
||||||
const direction = React.useRef(1);
|
|
||||||
const scrollerRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const [width, setWidth] = useState(initialWidth);
|
|
||||||
const [height, setHeight] = useState(initialHeight);
|
|
||||||
const containerRef = React.useRef<any>(null);
|
|
||||||
|
|
||||||
const size = useResizeObserver(scrollerRef);
|
|
||||||
|
|
||||||
const calculateJitter = (
|
|
||||||
i: number,
|
|
||||||
timestamp: number,
|
|
||||||
baseHeight: number,
|
|
||||||
minJitter: number,
|
|
||||||
jitterAmplitude: number
|
|
||||||
) => {
|
|
||||||
//
|
|
||||||
const jitterFactor = Math.sin(timestamp / 200 + i) * 0.5 + 0.5;
|
|
||||||
const jitter =
|
|
||||||
minJitter +
|
|
||||||
jitterFactor * (jitterAmplitude - minJitter) * (baseHeight / maxBarCount);
|
|
||||||
return jitter;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startAudioVisualization = () => {
|
|
||||||
if (!canvasRef.current || !analyserData.data?.length) return;
|
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const canvasCtx = canvas.getContext('2d');
|
|
||||||
if (!canvasCtx) return;
|
|
||||||
|
|
||||||
const WIDTH = (canvas.width = width * 2);
|
|
||||||
const HEIGHT = (canvas.height = height * 2);
|
|
||||||
|
|
||||||
if (!isScaled.current) {
|
|
||||||
canvasCtx.scale(2, 2);
|
|
||||||
isScaled.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const barWidth = 4;
|
|
||||||
const barSpacing = 6;
|
|
||||||
const centerLine = Math.floor(HEIGHT / 2);
|
|
||||||
|
|
||||||
const jitterAmplitude = amplitude;
|
|
||||||
const minJitter = 10;
|
|
||||||
|
|
||||||
let lastFrameTime = 0;
|
|
||||||
|
|
||||||
const gradient = canvasCtx.createLinearGradient(0, 0, 0, HEIGHT);
|
|
||||||
gradient.addColorStop(0, '#007BFF');
|
|
||||||
gradient.addColorStop(1, '#0069DA');
|
|
||||||
canvasCtx.fillStyle = gradient;
|
|
||||||
|
|
||||||
const draw = (timestamp: number) => {
|
|
||||||
const elapsed = timestamp - lastFrameTime;
|
|
||||||
if (elapsed < 16) {
|
|
||||||
animationId.current = requestAnimationFrame(draw);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastFrameTime = timestamp;
|
|
||||||
|
|
||||||
analyserData.analyser?.current?.getByteFrequencyData(analyserData.data);
|
|
||||||
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
|
||||||
|
|
||||||
const barCount = Math.min(maxBarCount, analyserData.data.length);
|
|
||||||
const totalWidth = barCount * (barWidth + barSpacing) - barSpacing;
|
|
||||||
let x = WIDTH / 2 - totalWidth / 2 + oscillationOffset.current;
|
|
||||||
|
|
||||||
oscillationOffset.current += direction.current;
|
|
||||||
if (Math.abs(oscillationOffset.current) > 20) {
|
|
||||||
direction.current *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < barCount; i++) {
|
|
||||||
const baseHeight = Math.floor(analyserData.data[i] / 2) * scaleFactor;
|
|
||||||
|
|
||||||
const jitter = calculateJitter(
|
|
||||||
i,
|
|
||||||
timestamp,
|
|
||||||
baseHeight,
|
|
||||||
minJitter,
|
|
||||||
jitterAmplitude
|
|
||||||
);
|
|
||||||
const barHeight = baseHeight + Math.round(jitter);
|
|
||||||
|
|
||||||
const topY = Math.round(centerLine - barHeight / 2);
|
|
||||||
const bottomY = Math.round(centerLine + barHeight / 2);
|
|
||||||
|
|
||||||
canvasCtx.beginPath();
|
|
||||||
canvasCtx.moveTo(x, bottomY);
|
|
||||||
canvasCtx.lineTo(x, topY + 2);
|
|
||||||
canvasCtx.arcTo(x + barWidth, topY + 2, x + barWidth, bottomY, 2);
|
|
||||||
canvasCtx.lineTo(x + barWidth, bottomY);
|
|
||||||
canvasCtx.closePath();
|
|
||||||
canvasCtx.fill();
|
|
||||||
|
|
||||||
x += barWidth + barSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
animationId.current = requestAnimationFrame(draw);
|
|
||||||
};
|
|
||||||
|
|
||||||
draw(performance.now());
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (size) {
|
|
||||||
if (maxWidth) {
|
|
||||||
setWidth(Math.min(size.width, maxWidth));
|
|
||||||
} else {
|
|
||||||
setWidth(size?.width || 0);
|
|
||||||
}
|
|
||||||
if (!fixedHeight) {
|
|
||||||
setHeight(size?.height || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [size, maxWidth]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canvasRef.current) return;
|
|
||||||
const clearCanvas = () => {
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) ctx.clearRect(0, 0, width * 2, height * 2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!analyserData.data?.length || !analyserData.analyser?.current) {
|
|
||||||
clearCanvas();
|
|
||||||
cancelAnimationFrame(animationId.current);
|
|
||||||
animationId.current = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
startAudioVisualization();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(animationId.current);
|
|
||||||
clearCanvas();
|
|
||||||
};
|
|
||||||
}, [analyserData, width, height]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="scroller-wrapper"
|
|
||||||
ref={scrollerRef}
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="canvas-wrap"
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
>
|
|
||||||
<canvas ref={canvasRef} style={{ display: 'block' }}></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AudioAnimation;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const AudioWrapper = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AudioElement: React.FC<any> = (props) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<AudioWrapper>
|
|
||||||
<audio {...props} controls></audio>
|
|
||||||
</AudioWrapper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AudioElement;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
export type AudioEvent =
|
|
||||||
| 'play'
|
|
||||||
| 'playing'
|
|
||||||
| 'pause'
|
|
||||||
| 'timeupdate'
|
|
||||||
| 'ended'
|
|
||||||
| 'loadedmetadata'
|
|
||||||
| 'audioprocess'
|
|
||||||
| 'canplay'
|
|
||||||
| 'ended'
|
|
||||||
| 'loadeddata'
|
|
||||||
| 'seeked'
|
|
||||||
| 'seeking'
|
|
||||||
| 'volumechange';
|
|
||||||
|
|
||||||
export interface AudioPlayerProps {
|
|
||||||
controls?: boolean;
|
|
||||||
autoplay?: boolean;
|
|
||||||
url: string;
|
|
||||||
speed?: number;
|
|
||||||
ref?: any;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
duration?: number;
|
|
||||||
onPlay?: () => void;
|
|
||||||
onPlaying?: () => void;
|
|
||||||
onPause?: () => void;
|
|
||||||
onTimeUpdate?: () => void;
|
|
||||||
onEnded?: () => void;
|
|
||||||
onLoadedMetadata?: (duration: number) => void;
|
|
||||||
onAudioProcess?: (current: number) => void;
|
|
||||||
onCanPlay?: () => void;
|
|
||||||
onLoadedData?: () => void;
|
|
||||||
onSeeked?: () => void;
|
|
||||||
onSeeking?: () => void;
|
|
||||||
onVolumeChange?: () => void;
|
|
||||||
onReady?: (duration: number) => void;
|
|
||||||
onAnalyse?: (analyseData: any, frequencyBinCount: any) => void;
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
.player-wrap {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
// background-color: var(--ant-color-fill-quaternary);
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
.player-ui {
|
|
||||||
padding: 8px 16px;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-btn {
|
|
||||||
margin-inline: 30px;
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
|
|
||||||
.ant-btn {
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.backward,
|
|
||||||
.forward {
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.slider-inner {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
width: 52px;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
margin-inline: 10px;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.slider {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
line-height: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
align-self: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-slider-horizontal {
|
|
||||||
margin-block: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker {
|
|
||||||
margin-left: 10px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.volume-slider {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { formatTime } from '@/utils/index';
|
|
||||||
import {
|
|
||||||
FastBackwardOutlined,
|
|
||||||
FastForwardOutlined,
|
|
||||||
PauseCircleFilled,
|
|
||||||
PlayCircleFilled
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Slider, Tooltip } from 'antd';
|
|
||||||
import { round } from 'lodash';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle
|
|
||||||
} from 'react';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
interface AudioPlayerProps {
|
|
||||||
autoplay?: boolean;
|
|
||||||
url: string;
|
|
||||||
speed?: number;
|
|
||||||
ref?: any;
|
|
||||||
name: string;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
duration?: number;
|
|
||||||
extra?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const speedOptions = [
|
|
||||||
{ label: '1x', value: 1 },
|
|
||||||
{ label: '2x', value: 2 },
|
|
||||||
{ label: '3x', value: 3 },
|
|
||||||
{ label: '4x', value: 4 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const speedConfig = {
|
|
||||||
min: 0.5,
|
|
||||||
max: 2,
|
|
||||||
step: 0.25
|
|
||||||
};
|
|
||||||
|
|
||||||
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { autoplay = false, speed: defaultSpeed = 1, extra } = props;
|
|
||||||
const audioRef = React.useRef<HTMLAudioElement>(null);
|
|
||||||
const [audioState, setAudioState] = React.useState<{
|
|
||||||
currentTime: number;
|
|
||||||
duration: number;
|
|
||||||
}>({
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
const [playOn, setPlayOn] = React.useState<boolean>(false);
|
|
||||||
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
|
|
||||||
const [volume, setVolume] = React.useState<number>(1);
|
|
||||||
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
|
|
||||||
const timer = React.useRef<any>(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
play: () => {
|
|
||||||
audioRef.current?.play();
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
const handleShowVolume = useCallback(() => {
|
|
||||||
setSpeakerOn(!speakerOn);
|
|
||||||
}, [speakerOn]);
|
|
||||||
|
|
||||||
const handleSeepdChange = useCallback((value: number | string) => {
|
|
||||||
setSpeed(value as number);
|
|
||||||
audioRef.current!.playbackRate = value as number;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAudioOnPlay = useCallback(() => {
|
|
||||||
console.log('audio play');
|
|
||||||
timer.current = setInterval(() => {
|
|
||||||
setAudioState((prestate) => {
|
|
||||||
return {
|
|
||||||
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
|
|
||||||
duration:
|
|
||||||
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (audioRef.current?.paused || audioRef.current?.ended) {
|
|
||||||
clearInterval(timer.current);
|
|
||||||
setPlayOn(false);
|
|
||||||
setAudioState((prestate: any) => {
|
|
||||||
return {
|
|
||||||
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
|
|
||||||
duration: prestate.duration
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePlay = useCallback(() => {
|
|
||||||
setPlayOn(!playOn);
|
|
||||||
if (playOn) {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
} else {
|
|
||||||
audioRef.current?.play();
|
|
||||||
}
|
|
||||||
}, [playOn]);
|
|
||||||
|
|
||||||
const handleFormatVolume = (val?: number) => {
|
|
||||||
if (val === undefined) {
|
|
||||||
return `${round(volume * 100)}%`;
|
|
||||||
}
|
|
||||||
return `${round(val * 100)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVolumeChange = useCallback((value: number) => {
|
|
||||||
audioRef.current!.volume = round(value, 2);
|
|
||||||
setVolume(round(value, 2));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const initPlayerConfig = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current!.volume = volume;
|
|
||||||
audioRef.current!.playbackRate = speed;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadedMetadata = useCallback(
|
|
||||||
(data: any) => {
|
|
||||||
const duration = Math.ceil(audioRef.current?.duration || 0);
|
|
||||||
setAudioState({
|
|
||||||
currentTime: 0,
|
|
||||||
duration:
|
|
||||||
duration && duration !== Infinity ? duration : props.duration || 0
|
|
||||||
});
|
|
||||||
setPlayOn(autoplay);
|
|
||||||
},
|
|
||||||
[autoplay, props.duration]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCurrentChange = useCallback((val: number) => {
|
|
||||||
audioRef.current!.currentTime = val;
|
|
||||||
setAudioState((prestate) => {
|
|
||||||
return {
|
|
||||||
currentTime: val,
|
|
||||||
duration: prestate.duration
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleReduceSpeed = () => {
|
|
||||||
setSpeed((pre) => {
|
|
||||||
if (pre - speedConfig.step < speedConfig.min) {
|
|
||||||
return speedConfig.min;
|
|
||||||
}
|
|
||||||
const next = pre - speedConfig.step;
|
|
||||||
audioRef.current!.playbackRate = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddSpeed = () => {
|
|
||||||
setSpeed((pre) => {
|
|
||||||
if (pre + speedConfig.step > speedConfig.max) {
|
|
||||||
return speedConfig.max;
|
|
||||||
}
|
|
||||||
const next = pre + speedConfig.step;
|
|
||||||
audioRef.current!.playbackRate = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnLoad = (e: any) => {
|
|
||||||
console.log('onload', e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownload = useCallback(() => {
|
|
||||||
const url = props.url || '';
|
|
||||||
const filename = props.name;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}, [props.url, props.name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
initPlayerConfig();
|
|
||||||
}
|
|
||||||
}, [audioRef.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearInterval(timer.current);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="player-wrap" style={{ width: props.width || '100%' }}>
|
|
||||||
<div className="player-ui">
|
|
||||||
<div className="play-content">
|
|
||||||
<div className="progress-bar">
|
|
||||||
<span className="file-name">{props.name}</span>
|
|
||||||
<div className="slider">
|
|
||||||
{/* <span className="time current">
|
|
||||||
{' '}
|
|
||||||
{formatTime(audioState.currentTime)}
|
|
||||||
</span> */}
|
|
||||||
<div className="slider-inner">
|
|
||||||
<Slider
|
|
||||||
tooltip={{ open: false }}
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
styles={{
|
|
||||||
rail: {
|
|
||||||
// height: 6
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
max={audioState.duration}
|
|
||||||
value={audioState.currentTime}
|
|
||||||
onChange={handleCurrentChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* <span className="time">{formatTime(audioState.duration)}</span> */}
|
|
||||||
</div>
|
|
||||||
<div className="controls">
|
|
||||||
<div className="audio-control flex-center">
|
|
||||||
<span className="time current">
|
|
||||||
{' '}
|
|
||||||
{formatTime(audioState.currentTime)}
|
|
||||||
</span>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: 'playground.audio.button.slow'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
className="backward"
|
|
||||||
disabled={
|
|
||||||
speed === speedConfig.min || speed < speedConfig.min
|
|
||||||
}
|
|
||||||
onClick={handleReduceSpeed}
|
|
||||||
>
|
|
||||||
<FastBackwardOutlined className="font-size-20" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<span className="play-btn">
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
type="text"
|
|
||||||
onClick={handlePlay}
|
|
||||||
disabled={!audioState?.duration}
|
|
||||||
icon={
|
|
||||||
!playOn ? (
|
|
||||||
<PlayCircleFilled
|
|
||||||
style={{ fontSize: '22px' }}
|
|
||||||
></PlayCircleFilled>
|
|
||||||
) : (
|
|
||||||
<PauseCircleFilled
|
|
||||||
style={{ fontSize: '22px' }}
|
|
||||||
></PauseCircleFilled>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></Button>
|
|
||||||
</span>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: 'playground.audio.button.fast'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
className="forward"
|
|
||||||
disabled={
|
|
||||||
speed === speedConfig.max || speed > speedConfig.max
|
|
||||||
}
|
|
||||||
onClick={handleAddSpeed}
|
|
||||||
>
|
|
||||||
<FastForwardOutlined className="font-size-20" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<span className="time">{formatTime(audioState.duration)}</span>
|
|
||||||
</div>
|
|
||||||
{extra}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<audio
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
autoPlay={autoplay}
|
|
||||||
src={props.url}
|
|
||||||
ref={audioRef}
|
|
||||||
preload="metadata"
|
|
||||||
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
|
|
||||||
onPlay={handleAudioOnPlay}
|
|
||||||
onLoadedMetadata={handleLoadedMetadata}
|
|
||||||
></audio>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default React.memo(AudioPlayer);
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef
|
|
||||||
} from 'react';
|
|
||||||
import { AudioPlayerProps } from './config/type';
|
|
||||||
|
|
||||||
const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
|
||||||
const { autoplay = false } = props;
|
|
||||||
const audioRef = React.useRef<HTMLAudioElement>(null);
|
|
||||||
|
|
||||||
// =================== audio context ======================
|
|
||||||
const audioContext = useRef<any>(null);
|
|
||||||
const analyser = useRef<any>(null);
|
|
||||||
const dataArray = useRef<any>(null);
|
|
||||||
// ========================================================
|
|
||||||
|
|
||||||
const initAudioContext = useCallback(() => {
|
|
||||||
audioContext.current = new (
|
|
||||||
window.AudioContext || (window as any).webkitAudioContext
|
|
||||||
)();
|
|
||||||
|
|
||||||
analyser.current = audioContext.current.createAnalyser();
|
|
||||||
analyser.current.fftSize = 512;
|
|
||||||
dataArray.current = new Uint8Array(analyser.current.frequencyBinCount);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const generateVisualData = useCallback(() => {
|
|
||||||
const source = audioContext.current.createMediaElementSource(
|
|
||||||
audioRef.current
|
|
||||||
);
|
|
||||||
source.connect(analyser.current);
|
|
||||||
analyser.current.connect(audioContext.current.destination);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const initEnvents = () => {
|
|
||||||
if (!audioRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('complete', () => {});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('play', () => {
|
|
||||||
props.onAnalyse?.(dataArray.current, analyser);
|
|
||||||
props.onPlay?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('pause', () => {
|
|
||||||
props.onAnalyse?.(dataArray.current, analyser);
|
|
||||||
props.onPause?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('timeupdate', () => {
|
|
||||||
const current = audioRef.current?.currentTime || 0;
|
|
||||||
props.onTimeUpdate?.();
|
|
||||||
props.onAudioProcess?.(current);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('ended', () => {
|
|
||||||
props.onEnded?.();
|
|
||||||
});
|
|
||||||
// add all other events
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('canplay', () => {
|
|
||||||
props.onCanPlay?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('loadeddata', () => {
|
|
||||||
initEnvents();
|
|
||||||
if (!audioContext.current) {
|
|
||||||
initAudioContext();
|
|
||||||
generateVisualData();
|
|
||||||
}
|
|
||||||
props.onLoadedData?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('seeked', () => {
|
|
||||||
props.onSeeked?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('seeking', () => {
|
|
||||||
props.onSeeking?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('volumechange', () => {
|
|
||||||
props.onVolumeChange?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('playing', () => {
|
|
||||||
props.onPlaying?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('loadedmetadata', () => {
|
|
||||||
const duration = audioRef.current?.duration || 0;
|
|
||||||
props.onLoadedMetadata?.(duration);
|
|
||||||
props.onReady?.(duration);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('ended', () => {
|
|
||||||
props.onEnded?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('loadeddata', () => {
|
|
||||||
props.onLoadedData?.();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
play: () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
// If playback has ended, reset to beginning
|
|
||||||
if (audioRef.current.currentTime >= audioRef.current.duration) {
|
|
||||||
audioRef.current.currentTime = 0;
|
|
||||||
}
|
|
||||||
audioRef.current.play();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
},
|
|
||||||
seekTo: (ratio: number) => {
|
|
||||||
if (audioRef.current && audioRef.current.duration) {
|
|
||||||
audioRef.current.currentTime = ratio * audioRef.current.duration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Compatibility layer for wavesurfer interface
|
|
||||||
wavesurfer: {
|
|
||||||
current: {
|
|
||||||
play: () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
// If playback has ended, reset to beginning
|
|
||||||
if (audioRef.current.currentTime >= audioRef.current.duration) {
|
|
||||||
audioRef.current.currentTime = 0;
|
|
||||||
}
|
|
||||||
return audioRef.current.play();
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
isPlaying: () => {
|
|
||||||
return audioRef.current ? !audioRef.current?.paused : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
initEnvents();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (audioContext.current) {
|
|
||||||
audioContext.current.close();
|
|
||||||
}
|
|
||||||
// remove all events
|
|
||||||
|
|
||||||
audioRef.current?.removeEventListener('play', () => {});
|
|
||||||
audioRef.current?.removeEventListener('pause', () => {});
|
|
||||||
audioRef.current?.removeEventListener('timeupdate', () => {});
|
|
||||||
audioRef.current?.removeEventListener('ended', () => {});
|
|
||||||
audioRef.current?.removeEventListener('canplay', () => {});
|
|
||||||
audioRef.current?.removeEventListener('loadeddata', () => {});
|
|
||||||
audioRef.current?.removeEventListener('seeked', () => {});
|
|
||||||
audioRef.current?.removeEventListener('seeking', () => {});
|
|
||||||
audioRef.current?.removeEventListener('volumechange', () => {});
|
|
||||||
audioRef.current?.removeEventListener('playing', () => {});
|
|
||||||
audioRef.current?.removeEventListener('loadedmetadata', () => {});
|
|
||||||
audioRef.current?.removeEventListener('ended', () => {});
|
|
||||||
audioRef.current?.removeEventListener('loadeddata', () => {});
|
|
||||||
};
|
|
||||||
}, [audioRef.current]);
|
|
||||||
|
|
||||||
// Reload audio when URL changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (audioRef.current && props.url) {
|
|
||||||
audioRef.current.load();
|
|
||||||
}
|
|
||||||
}, [props.url]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
autoPlay={autoplay}
|
|
||||||
src={props.url}
|
|
||||||
ref={audioRef}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '-9999px',
|
|
||||||
opacity: 0
|
|
||||||
}}
|
|
||||||
preload="metadata"
|
|
||||||
></audio>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default RawAudioPlayer;
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
import { formatTime } from '@/utils/index';
|
|
||||||
import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Dropdown, Slider, type MenuProps } from 'antd';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import { round } from 'lodash';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo
|
|
||||||
} from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import AutoTooltip from '../auto-tooltip';
|
|
||||||
import IconFont from '../icon-font';
|
|
||||||
|
|
||||||
type ActionItem = 'download' | 'delete' | 'speed';
|
|
||||||
|
|
||||||
interface AudioPlayerProps {
|
|
||||||
autoplay?: boolean;
|
|
||||||
url: string;
|
|
||||||
speed?: number;
|
|
||||||
ref?: any;
|
|
||||||
name: string;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
duration?: number;
|
|
||||||
actions?: ActionItem[];
|
|
||||||
onDelete?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SliderWrapper = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
.ant-slider {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const isDarkMode = token.darkMode as boolean;
|
|
||||||
return {
|
|
||||||
wrapper: css`
|
|
||||||
position: relative;
|
|
||||||
min-width: 360px;
|
|
||||||
height: 54px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 8px 10px;
|
|
||||||
background-color: ${isDarkMode
|
|
||||||
? 'var(--ant-color-fill-secondary)'
|
|
||||||
: '#F1F3F4'};
|
|
||||||
border-radius: 28px;
|
|
||||||
.inner {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex: 1;
|
|
||||||
gap: 8px;
|
|
||||||
.slider {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1;
|
|
||||||
.ant-slider {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.ant-slider-handle {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:focus-within {
|
|
||||||
.ant-slider-handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-slider-handle {
|
|
||||||
opacity: 0;
|
|
||||||
&::before {
|
|
||||||
background-color: var(--ant-color-bg-spotlight);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const sliderStyles = {
|
|
||||||
rail: {
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: 'var(--ant-color-fill-secondary)'
|
|
||||||
},
|
|
||||||
track: {
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: 'var(--ant-color-bg-spotlight)'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const speedOptions = [
|
|
||||||
{ label: '1x', value: 1 },
|
|
||||||
{ label: '2x', value: 2 },
|
|
||||||
{ label: '3x', value: 3 },
|
|
||||||
{ label: '4x', value: 4 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const speedConfig = {
|
|
||||||
min: 0.5,
|
|
||||||
max: 2,
|
|
||||||
step: 0.25
|
|
||||||
};
|
|
||||||
|
|
||||||
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const {
|
|
||||||
autoplay = false,
|
|
||||||
speed: defaultSpeed = 1,
|
|
||||||
actions = ['delete'],
|
|
||||||
name,
|
|
||||||
onDelete
|
|
||||||
} = props;
|
|
||||||
const audioRef = React.useRef<HTMLAudioElement>(null);
|
|
||||||
const [audioState, setAudioState] = React.useState<{
|
|
||||||
currentTime: number;
|
|
||||||
duration: number;
|
|
||||||
}>({
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
console.log('audioState', name);
|
|
||||||
const [playOn, setPlayOn] = React.useState<boolean>(false);
|
|
||||||
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
|
|
||||||
const [volume, setVolume] = React.useState<number>(1);
|
|
||||||
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
|
|
||||||
const timer = React.useRef<any>(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
play: () => {
|
|
||||||
audioRef.current?.play();
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
const handleShowVolume = useCallback(() => {
|
|
||||||
setSpeakerOn(!speakerOn);
|
|
||||||
}, [speakerOn]);
|
|
||||||
|
|
||||||
const handleSeepdChange = useCallback((value: number | string) => {
|
|
||||||
setSpeed(value as number);
|
|
||||||
audioRef.current!.playbackRate = value as number;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAudioOnPlay = useCallback(() => {
|
|
||||||
timer.current = setInterval(() => {
|
|
||||||
setAudioState((prestate) => {
|
|
||||||
return {
|
|
||||||
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
|
|
||||||
duration:
|
|
||||||
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (audioRef.current?.paused || audioRef.current?.ended) {
|
|
||||||
clearInterval(timer.current);
|
|
||||||
setPlayOn(false);
|
|
||||||
setAudioState((prestate: any) => {
|
|
||||||
return {
|
|
||||||
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
|
|
||||||
duration: prestate.duration
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePlay = useCallback(() => {
|
|
||||||
setPlayOn(!playOn);
|
|
||||||
if (playOn) {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
} else {
|
|
||||||
audioRef.current?.play();
|
|
||||||
}
|
|
||||||
}, [playOn]);
|
|
||||||
|
|
||||||
const handleFormatVolume = (val?: number) => {
|
|
||||||
if (val === undefined) {
|
|
||||||
return `${round(volume * 100)}%`;
|
|
||||||
}
|
|
||||||
return `${round(val * 100)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVolumeChange = useCallback((value: number) => {
|
|
||||||
audioRef.current!.volume = round(value, 2);
|
|
||||||
setVolume(round(value, 2));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const initPlayerConfig = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current!.volume = volume;
|
|
||||||
audioRef.current!.playbackRate = speed;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadedMetadata = useCallback(
|
|
||||||
(data: any) => {
|
|
||||||
const duration = Math.ceil(audioRef.current?.duration || 0);
|
|
||||||
setAudioState({
|
|
||||||
currentTime: 0,
|
|
||||||
duration:
|
|
||||||
duration && duration !== Infinity ? duration : props.duration || 0
|
|
||||||
});
|
|
||||||
setPlayOn(autoplay);
|
|
||||||
},
|
|
||||||
[autoplay, props.duration]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCurrentChange = useCallback((val: number) => {
|
|
||||||
audioRef.current!.currentTime = val;
|
|
||||||
setAudioState((prestate) => {
|
|
||||||
return {
|
|
||||||
currentTime: val,
|
|
||||||
duration: prestate.duration
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleReduceSpeed = () => {
|
|
||||||
setSpeed((pre) => {
|
|
||||||
if (pre - speedConfig.step < speedConfig.min) {
|
|
||||||
return speedConfig.min;
|
|
||||||
}
|
|
||||||
const next = pre - speedConfig.step;
|
|
||||||
audioRef.current!.playbackRate = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddSpeed = () => {
|
|
||||||
setSpeed((pre) => {
|
|
||||||
if (pre + speedConfig.step > speedConfig.max) {
|
|
||||||
return speedConfig.max;
|
|
||||||
}
|
|
||||||
const next = pre + speedConfig.step;
|
|
||||||
audioRef.current!.playbackRate = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnLoad = (e: any) => {
|
|
||||||
console.log('onload', e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownload = useCallback(() => {
|
|
||||||
const url = props.url || '';
|
|
||||||
const filename = props.name;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename || 'audio.mp3'; // Default filename
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}, [props.url, props.name]);
|
|
||||||
|
|
||||||
const items: MenuProps['items'] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'download',
|
|
||||||
label: intl.formatMessage({ id: 'common.button.download' }),
|
|
||||||
icon: <DownloadOutlined />,
|
|
||||||
onClick: onDownload
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'speed',
|
|
||||||
label: intl.formatMessage({ id: 'playground.params.speed' }),
|
|
||||||
icon: <IconFont type="icon-play-speed"></IconFont>,
|
|
||||||
children: speedOptions.map((item) => ({
|
|
||||||
key: item.value,
|
|
||||||
label: item.label,
|
|
||||||
onClick: () => handleSeepdChange(item.value)
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
label: intl.formatMessage({ id: 'common.button.delete' }),
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
danger: true,
|
|
||||||
onClick: () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.pause();
|
|
||||||
audioRef.current.src = '';
|
|
||||||
audioRef.current.load();
|
|
||||||
}
|
|
||||||
setAudioState({ currentTime: 0, duration: 0 });
|
|
||||||
setPlayOn(false);
|
|
||||||
onDelete?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
].filter((item) => actions.includes(item.key as ActionItem));
|
|
||||||
}, [actions, intl, onDownload, onDelete, handleSeepdChange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
initPlayerConfig();
|
|
||||||
}
|
|
||||||
}, [audioRef.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearInterval(timer.current);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.wrapper}
|
|
||||||
style={{
|
|
||||||
width: props.width || '100%',
|
|
||||||
height: props.height || '60px',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="inner">
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
type="text"
|
|
||||||
onClick={handlePlay}
|
|
||||||
shape="circle"
|
|
||||||
disabled={!audioState?.duration}
|
|
||||||
icon={
|
|
||||||
!playOn ? (
|
|
||||||
<IconFont
|
|
||||||
type="icon-playcircle-fill"
|
|
||||||
style={{ fontSize: '24px' }}
|
|
||||||
></IconFont>
|
|
||||||
) : (
|
|
||||||
<IconFont
|
|
||||||
type="icon-stopcircle-fill"
|
|
||||||
style={{ fontSize: '24px' }}
|
|
||||||
></IconFont>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></Button>
|
|
||||||
|
|
||||||
<div className="slider">
|
|
||||||
<div className="flex-center flex-between file-name">
|
|
||||||
<AutoTooltip ghost maxWidth={200}>
|
|
||||||
<span>{name}</span>
|
|
||||||
</AutoTooltip>
|
|
||||||
</div>
|
|
||||||
<SliderWrapper>
|
|
||||||
<span className="time">{formatTime(audioState.currentTime)}</span>
|
|
||||||
<Slider
|
|
||||||
tooltip={{ open: false }}
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
styles={sliderStyles}
|
|
||||||
max={audioState.duration}
|
|
||||||
value={audioState.currentTime}
|
|
||||||
onChange={handleCurrentChange}
|
|
||||||
/>
|
|
||||||
</SliderWrapper>
|
|
||||||
</div>
|
|
||||||
<Dropdown menu={{ items }} trigger={['click']}>
|
|
||||||
<Button
|
|
||||||
icon={<IconFont type="icon-more"></IconFont>}
|
|
||||||
type="text"
|
|
||||||
size="middle"
|
|
||||||
shape="circle"
|
|
||||||
></Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<audio
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
autoPlay={autoplay}
|
|
||||||
src={props.url}
|
|
||||||
ref={audioRef}
|
|
||||||
preload="metadata"
|
|
||||||
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
|
|
||||||
onPlay={handleAudioOnPlay}
|
|
||||||
onLoadedMetadata={handleLoadedMetadata}
|
|
||||||
></audio>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default React.memo(AudioPlayer);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
.toolbar-wrapper {
|
|
||||||
padding: 0 24px;
|
|
||||||
color: rgba(255, 255, 255, 65%);
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: rgba(0, 0, 0, 10%);
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-wrapper .anticon {
|
|
||||||
padding: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-wrapper .anticon[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-wrapper .anticon:hover {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import fallbackImg from '@/assets/images/img_fallback.png';
|
|
||||||
import {
|
|
||||||
DownloadOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
RotateLeftOutlined,
|
|
||||||
RotateRightOutlined,
|
|
||||||
SwapOutlined,
|
|
||||||
UndoOutlined,
|
|
||||||
ZoomInOutlined,
|
|
||||||
ZoomOutOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Image as AntImage, ImageProps, Space } from 'antd';
|
|
||||||
import { round } from 'lodash';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
const AutoImage: React.FC<
|
|
||||||
ImageProps & {
|
|
||||||
height: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
autoSize?: boolean;
|
|
||||||
preview?: boolean;
|
|
||||||
onLoad?: () => void;
|
|
||||||
}
|
|
||||||
> = (props) => {
|
|
||||||
const { height = 100, width: w, autoSize, preview = true, ...rest } = props;
|
|
||||||
const [width, setWidth] = useState(w || 0);
|
|
||||||
const [isError, setIsError] = useState(false);
|
|
||||||
|
|
||||||
const getImgRatio = useCallback((url: string): Promise<{ ratio: number }> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
resolve({ ratio: round(img.width / img.height, 2) });
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
resolve({ ratio: 1 });
|
|
||||||
};
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnLoad = useCallback(async () => {
|
|
||||||
if (autoSize) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { ratio } = await getImgRatio(props.src || '');
|
|
||||||
|
|
||||||
if (typeof height === 'number') {
|
|
||||||
setWidth(height * ratio);
|
|
||||||
} else {
|
|
||||||
throw new Error('Height must be a number');
|
|
||||||
}
|
|
||||||
}, [getImgRatio, height, props.src]);
|
|
||||||
|
|
||||||
const onDownload = useCallback(() => {
|
|
||||||
const url = props.src || '';
|
|
||||||
const filename = Date.now() + '';
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}, [props.src]);
|
|
||||||
|
|
||||||
const handleImgLoad = useCallback(() => {
|
|
||||||
props.onLoad?.();
|
|
||||||
setIsError(false);
|
|
||||||
}, [props.onLoad]);
|
|
||||||
|
|
||||||
const handleOnError = useCallback((e: any) => {
|
|
||||||
setIsError(true);
|
|
||||||
e.target.src = fallbackImg;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleOnLoad();
|
|
||||||
}, [handleOnLoad]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setWidth(w || 0);
|
|
||||||
}, [w]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AntImage
|
|
||||||
{...rest}
|
|
||||||
height={isError ? 'auto' : height}
|
|
||||||
width={isError ? '100%' : width}
|
|
||||||
onError={handleOnError}
|
|
||||||
onLoad={handleImgLoad}
|
|
||||||
fallback={fallbackImg}
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
preview={
|
|
||||||
preview &&
|
|
||||||
!isError && {
|
|
||||||
mask: <EyeOutlined />,
|
|
||||||
actionsRender: (
|
|
||||||
_,
|
|
||||||
{
|
|
||||||
transform: { scale },
|
|
||||||
actions: {
|
|
||||||
onFlipY,
|
|
||||||
onFlipX,
|
|
||||||
onRotateLeft,
|
|
||||||
onRotateRight,
|
|
||||||
onZoomOut,
|
|
||||||
onZoomIn,
|
|
||||||
onReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) => (
|
|
||||||
<Space size={12} className="toolbar-wrapper">
|
|
||||||
<DownloadOutlined onClick={onDownload} />
|
|
||||||
<SwapOutlined rotate={90} onClick={onFlipY} />
|
|
||||||
<SwapOutlined onClick={onFlipX} />
|
|
||||||
<RotateLeftOutlined onClick={onRotateLeft} />
|
|
||||||
<RotateRightOutlined onClick={onRotateRight} />
|
|
||||||
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
|
|
||||||
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
|
|
||||||
<UndoOutlined onClick={onReset} />
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoImage;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.img-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-wrapper .auto-image {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-wrapper .progress-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-square {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-square-bg {
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-square-fg {
|
|
||||||
fill: none;
|
|
||||||
stroke-linecap: square;
|
|
||||||
stroke-dasharray: 400;
|
|
||||||
stroke-dashoffset: 400;
|
|
||||||
transition: stroke-dashoffset 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
position: absolute;
|
|
||||||
color: black;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
.thumb-img {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
left: 4px;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0 8px;
|
|
||||||
background-color: var(--ant-geekblue-1);
|
|
||||||
z-index: 10;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-progress-wrap {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(0, 0, 0, 30%);
|
|
||||||
|
|
||||||
.ant-progress-text {
|
|
||||||
color: var(--color-white-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.img {
|
|
||||||
display: flex;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
cursor: pointer;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.del {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: var(--font-size-middle);
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--color-white-1);
|
|
||||||
display: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.ant-image .ant-image-mask {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity var(--ant-motion-duration-slow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.del {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-image {
|
|
||||||
// height: 100%;
|
|
||||||
width: inherit;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.auto-bg-color {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
filter: blur(100px);
|
|
||||||
backdrop-filter: blur(100px);
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb-img {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.img {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-image {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mask {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
||||||
import { Progress, ProgressProps, Spin } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { round } from 'lodash';
|
|
||||||
import ResizeObserver from 'rc-resize-observer';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import AutoImage from './index';
|
|
||||||
import './single-image.less';
|
|
||||||
interface SingleImageProps {
|
|
||||||
loading?: boolean;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
progress?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
dataUrl: string;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
uid: number | string;
|
|
||||||
preview?: boolean;
|
|
||||||
autoSize?: boolean;
|
|
||||||
onDelete: (uid: number | string) => void;
|
|
||||||
onClick?: (item: any) => void;
|
|
||||||
autoBgColor?: boolean;
|
|
||||||
editable?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
loadingSize?: ProgressProps['size'];
|
|
||||||
progressType?: 'line' | 'circle' | 'dashboard';
|
|
||||||
progressColor?: string;
|
|
||||||
progressWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SingleImage: React.FC<SingleImageProps> = (props) => {
|
|
||||||
const {
|
|
||||||
editable,
|
|
||||||
onDelete,
|
|
||||||
onClick,
|
|
||||||
autoSize,
|
|
||||||
uid,
|
|
||||||
loading,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
progress,
|
|
||||||
maxHeight,
|
|
||||||
maxWidth,
|
|
||||||
dataUrl = '',
|
|
||||||
label,
|
|
||||||
style,
|
|
||||||
autoBgColor,
|
|
||||||
preview = true,
|
|
||||||
loadingSize = 'default'
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const imgWrapper = React.useRef<HTMLSpanElement>(null);
|
|
||||||
const [imgSize, setImgSize] = React.useState({
|
|
||||||
width: width,
|
|
||||||
height: height
|
|
||||||
});
|
|
||||||
|
|
||||||
const thumImgWrapStyle = React.useMemo(() => {
|
|
||||||
return loading ? { width: '100%', height: '100%' } : {};
|
|
||||||
}, [loading, imgSize]);
|
|
||||||
|
|
||||||
const handleOnClick = useCallback(() => {
|
|
||||||
onClick?.(props);
|
|
||||||
}, [onClick, props]);
|
|
||||||
|
|
||||||
const handleResize = useCallback(
|
|
||||||
(size: { width: number; height: number }) => {
|
|
||||||
if (!autoSize || !size.width || !size.height) return;
|
|
||||||
|
|
||||||
const { width: containerWidth, height: containerHeight } = size;
|
|
||||||
const { width: originalWidth, height: originalHeight } = props;
|
|
||||||
|
|
||||||
if (!originalWidth || !originalHeight) return;
|
|
||||||
|
|
||||||
const widthRatio = containerWidth / originalWidth;
|
|
||||||
const heightRatio = containerHeight / originalHeight;
|
|
||||||
|
|
||||||
const scale = Math.min(widthRatio, heightRatio, 1);
|
|
||||||
|
|
||||||
const newWidth = originalWidth * scale;
|
|
||||||
const newHeight = originalHeight * scale;
|
|
||||||
|
|
||||||
if (newWidth === imgSize.width && newHeight === imgSize.height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImgSize({
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[autoSize, props.width, props.height]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnLoad = React.useCallback(async () => {}, []);
|
|
||||||
|
|
||||||
const handleOnDelete = (uid: number | string, e: any) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete(uid);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderProgress = () => {
|
|
||||||
<Progress
|
|
||||||
percent={round(progress, 0)}
|
|
||||||
type="dashboard"
|
|
||||||
size={loadingSize}
|
|
||||||
steps={{ count: 50, gap: 2 }}
|
|
||||||
format={() => <span className="font-size-20">{round(progress, 0)}%</span>}
|
|
||||||
railColor="var(--ant-color-fill-secondary)"
|
|
||||||
/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizeObserver onResize={handleResize}>
|
|
||||||
<div
|
|
||||||
style={{ ...style }}
|
|
||||||
key={uid}
|
|
||||||
className={classNames('single-image', {
|
|
||||||
'auto-bg-color': autoBgColor,
|
|
||||||
'auto-size': autoSize,
|
|
||||||
loading: loading
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{autoBgColor && (
|
|
||||||
<div
|
|
||||||
className="mask"
|
|
||||||
style={{
|
|
||||||
background: `url(${dataUrl}) center center / cover no-repeat`
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="thumb-img"
|
|
||||||
style={{ ...thumImgWrapStyle }}
|
|
||||||
ref={imgWrapper}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{label && <div className="label">{label}</div>}
|
|
||||||
{loading ? (
|
|
||||||
<span
|
|
||||||
className="progress-wrap"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
border: '1px solid var(--ant-color-split)',
|
|
||||||
borderRadius: 'var(--border-radius-base)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '10px',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spin
|
|
||||||
size="middle"
|
|
||||||
indicator={<LoadingOutlined style={{ fontSize: 32 }} spin />}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
onClick={handleOnClick}
|
|
||||||
className="img"
|
|
||||||
style={{
|
|
||||||
maxHeight: `min(${maxHeight}, 100%)`,
|
|
||||||
maxWidth: `min(${maxWidth}, 100%)`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AutoImage
|
|
||||||
style={{ objectFit: 'cover' }}
|
|
||||||
preview={preview}
|
|
||||||
autoSize={autoSize}
|
|
||||||
src={dataUrl}
|
|
||||||
width={imgSize.width || '100%'}
|
|
||||||
height={imgSize.height || 100}
|
|
||||||
onLoad={handleOnLoad}
|
|
||||||
/>
|
|
||||||
{progress && progress < 100 && (
|
|
||||||
<span className="small-progress-wrap">
|
|
||||||
<Progress
|
|
||||||
percent={round(progress, 0)}
|
|
||||||
type="dashboard"
|
|
||||||
size="small"
|
|
||||||
steps={{ count: 25, gap: 3 }}
|
|
||||||
format={() => (
|
|
||||||
<span className="font-size-12">
|
|
||||||
{round(progress, 0)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
strokeColor="var(--color-white-secondary)"
|
|
||||||
railColor="var(--ant-color-fill-secondary)"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
|
|
||||||
{editable && (
|
|
||||||
<span className="del" onClick={(e) => handleOnDelete(uid, e)}>
|
|
||||||
<CloseCircleOutlined />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</ResizeObserver>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SingleImage;
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import { CloseOutlined } from '@ant-design/icons';
|
|
||||||
import { Tag, Tooltip, type TagProps } from 'antd';
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import React, {
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { TooltipOverlayScroller } from '../overlay-scroller';
|
|
||||||
|
|
||||||
// type TagProps = React.ComponentProps<typeof Tag>;
|
|
||||||
|
|
||||||
interface AutoTooltipProps extends Omit<TagProps, 'title'> {
|
|
||||||
children: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
minWidth?: number | string;
|
|
||||||
color?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
ghost?: boolean;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
showTitle?: boolean;
|
|
||||||
closable?: boolean;
|
|
||||||
radius?: number | string;
|
|
||||||
filled?: boolean;
|
|
||||||
tooltipProps?: React.ComponentProps<typeof Tooltip>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledTag = styled(Tag)`
|
|
||||||
margin: 0;
|
|
||||||
&.tag-filled {
|
|
||||||
border: none;
|
|
||||||
background-color: var(--ant-color-fill-secondary);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
|
||||||
children,
|
|
||||||
maxWidth = '100%',
|
|
||||||
minWidth,
|
|
||||||
ghost = false,
|
|
||||||
title,
|
|
||||||
showTitle = false,
|
|
||||||
tooltipProps,
|
|
||||||
radius = 12,
|
|
||||||
filled = false,
|
|
||||||
...tagProps
|
|
||||||
}) => {
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
||||||
const resizeObserver = useRef<ResizeObserver | null>(null);
|
|
||||||
|
|
||||||
const checkOverflow = useCallback(() => {
|
|
||||||
if (contentRef.current) {
|
|
||||||
const { scrollWidth, clientWidth } = contentRef.current;
|
|
||||||
setIsOverflowing(scrollWidth > clientWidth);
|
|
||||||
}
|
|
||||||
}, [contentRef.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const element = contentRef.current;
|
|
||||||
if (!element) return;
|
|
||||||
resizeObserver.current?.disconnect();
|
|
||||||
resizeObserver.current = new ResizeObserver(() => {
|
|
||||||
checkOverflow();
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserver.current?.observe(element);
|
|
||||||
|
|
||||||
// Initial check
|
|
||||||
checkOverflow();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.current?.disconnect();
|
|
||||||
resizeObserver.current = null;
|
|
||||||
};
|
|
||||||
}, [checkOverflow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const debouncedCheckOverflow = throttle(checkOverflow, 200);
|
|
||||||
window.addEventListener('resize', debouncedCheckOverflow);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', debouncedCheckOverflow);
|
|
||||||
debouncedCheckOverflow.cancel();
|
|
||||||
};
|
|
||||||
}, [checkOverflow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkOverflow();
|
|
||||||
}, [children, checkOverflow]);
|
|
||||||
|
|
||||||
const tagStyle = useMemo(
|
|
||||||
() => ({
|
|
||||||
maxWidth,
|
|
||||||
minWidth,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap' as const,
|
|
||||||
...tagProps.style
|
|
||||||
}),
|
|
||||||
[maxWidth, tagProps.style]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipOverlayScroller
|
|
||||||
toolTipProps={{
|
|
||||||
...tooltipProps,
|
|
||||||
destroyOnHidden: false
|
|
||||||
}}
|
|
||||||
title={isOverflowing || showTitle ? title || children : false}
|
|
||||||
>
|
|
||||||
{ghost ? (
|
|
||||||
<div ref={contentRef} style={tagStyle} data-overflow={isOverflowing}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<StyledTag
|
|
||||||
{...tagProps}
|
|
||||||
variant="outlined"
|
|
||||||
className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`}
|
|
||||||
ref={contentRef}
|
|
||||||
style={{
|
|
||||||
paddingInline: tagProps.closable ? '8px 22px' : 8,
|
|
||||||
borderRadius: radius,
|
|
||||||
...tagStyle
|
|
||||||
}}
|
|
||||||
closeIcon={
|
|
||||||
tagProps.closable ? (
|
|
||||||
<CloseOutlined
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: 8,
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translateY(-50%)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledTag>
|
|
||||||
)}
|
|
||||||
</TooltipOverlayScroller>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoTooltip;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { OverlayScroller } from '@/components/overlay-scroller';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface TitleTipProps {
|
|
||||||
isOverflowing: boolean;
|
|
||||||
showTitle: boolean;
|
|
||||||
title: React.ReactNode;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TitleTip: React.FC<TitleTipProps> = (props) => {
|
|
||||||
const { isOverflowing, showTitle, title, children } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayScroller maxHeight={200}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 'fit-content',
|
|
||||||
maxWidth: 'var(--width-tooltip-max)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isOverflowing || showTitle ? title || children : ''}
|
|
||||||
</div>
|
|
||||||
</OverlayScroller>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(TitleTip);
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import bibtexParse from '@orcid/bibtex-parse-js';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
/*
|
|
||||||
@inproceedings{Lysenko:2010:GMC:1839778.1839781,\
|
|
||||||
author = {Lysenko, Mikola and Nelaturi, Saigopal and Shapiro, Vadim},\
|
|
||||||
title = {Group morphology with convolution algebras},\
|
|
||||||
booktitle = {Proceedings of the 14th ACM Symposium on Solid and Physical Modeling},\
|
|
||||||
series = {SPM '10},\
|
|
||||||
year = {2010},\
|
|
||||||
isbn = {978-1-60558-984-8},\
|
|
||||||
location = {Haifa, Israel},\
|
|
||||||
pages = {11--22},\
|
|
||||||
numpages = {12},\
|
|
||||||
url = {http://doi.acm.org/10.1145/1839778.1839781},\
|
|
||||||
doi = {10.1145/1839778.1839781},\
|
|
||||||
acmid = {1839781},\
|
|
||||||
publisher = {ACM},\
|
|
||||||
address = {New York, NY, USA},\
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BibTeXViewer: React.FC<{ data: string }> = ({ data }) => {
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const dataList = bibtexParse.toJSON(data);
|
|
||||||
return (
|
|
||||||
<ol>
|
|
||||||
{dataList.map((item: any, index: number) => (
|
|
||||||
<li key={index} style={{ lineHeight: 2 }}>
|
|
||||||
<Typography.Link href={item.entryTags?.url} target="_blank">
|
|
||||||
{item.entryTags?.title}.{' '}
|
|
||||||
</Typography.Link>
|
|
||||||
<Typography.Text>{item.entryTags?.author}. </Typography.Text>
|
|
||||||
<Typography.Text>[{item.entryTags?.year}] </Typography.Text>
|
|
||||||
{item.entryTags?.journal && (
|
|
||||||
<Typography.Text>.({item.entryTags?.journal})</Typography.Text>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BibTeXViewer;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { DoubleRightOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
interface MoreButtonProps {
|
|
||||||
show: boolean;
|
|
||||||
loadMore: () => void;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MoreWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-block: 16px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
&.loading {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MoreButton: React.FC<MoreButtonProps> = (props) => {
|
|
||||||
const { show, loading, loadMore } = props;
|
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{show ? (
|
|
||||||
<MoreWrapper className={loading ? 'loading' : ''}>
|
|
||||||
<Button
|
|
||||||
onClick={loadMore}
|
|
||||||
size="middle"
|
|
||||||
type="text"
|
|
||||||
icon={<DoubleRightOutlined rotate={90} />}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: 'common.button.more' })}
|
|
||||||
</Button>
|
|
||||||
</MoreWrapper>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MoreButton;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
background-color: var(--ant-color-bg-container);
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CardWrapper = (props: any) => {
|
|
||||||
const { children, style } = props;
|
|
||||||
return <Wrapper style={{ ...style }}>{children}</Wrapper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CardWrapper;
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { createStyles } from 'antd-style';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const SimpleCardItemWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
gap: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => ({
|
|
||||||
wrapper: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: ${token.colorBgContainer};
|
|
||||||
border-radius: ${token.borderRadius}px;
|
|
||||||
padding: ${token.padding}px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: ${token.padding}px;
|
|
||||||
&.bordered {
|
|
||||||
border: 1px solid ${token.colorBorder};
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: ${token.fontSize}px;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: ${token.fontSize}px;
|
|
||||||
color: ${token.colorTextSecondary};
|
|
||||||
gap: 8px;
|
|
||||||
.icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
gap: 10px;
|
|
||||||
&.roundRect {
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
&.circle {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}));
|
|
||||||
export const SimpleCardItem: React.FC<{
|
|
||||||
title?: string;
|
|
||||||
content?: React.ReactNode;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
bordered?: boolean;
|
|
||||||
color?: string;
|
|
||||||
iconType?: string;
|
|
||||||
}> = (props) => {
|
|
||||||
const { styles, cx } = useStyles();
|
|
||||||
|
|
||||||
const { title, content, style, bordered, iconType, color } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx({ bordered: bordered }, styles.wrapper)} style={style}>
|
|
||||||
<div className="title">{title}</div>
|
|
||||||
<div className="content">
|
|
||||||
{iconType && (
|
|
||||||
<span
|
|
||||||
className={cx([iconType], 'icon')}
|
|
||||||
style={{
|
|
||||||
backgroundColor: color || 'transparent'
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
<span>{content}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SimpleCard: React.FC<{
|
|
||||||
dataList: {
|
|
||||||
label: string;
|
|
||||||
value: React.ReactNode;
|
|
||||||
color: string;
|
|
||||||
iconType?: string;
|
|
||||||
}[];
|
|
||||||
height?: string | number;
|
|
||||||
bordered?: boolean;
|
|
||||||
styles?: {
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
item?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
}> = (props) => {
|
|
||||||
const { dataList, bordered, styles } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
|
|
||||||
{dataList.map((item, index) => (
|
|
||||||
<SimpleCardItem
|
|
||||||
key={index}
|
|
||||||
title={item.label}
|
|
||||||
content={item.value}
|
|
||||||
bordered={bordered}
|
|
||||||
color={item.color}
|
|
||||||
iconType={item.iconType}
|
|
||||||
style={{
|
|
||||||
...styles?.item
|
|
||||||
}}
|
|
||||||
></SimpleCardItem>
|
|
||||||
))}
|
|
||||||
</SimpleCardItemWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Button } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface CheckButtonsProps {
|
|
||||||
options: Global.BaseOption<string | number>[];
|
|
||||||
onChange: (value: string | number) => void;
|
|
||||||
cancelable?: boolean;
|
|
||||||
size?: 'small' | 'middle' | 'large';
|
|
||||||
type?: 'text' | 'primary' | 'default' | 'dashed' | 'link' | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CheckButtons: React.FC<CheckButtonsProps> = (props) => {
|
|
||||||
const [type, setType] = React.useState(props.type || 'text');
|
|
||||||
const [active, setActive] = React.useState<string | number | null>(null);
|
|
||||||
const handleChange = (value: string | number) => {
|
|
||||||
props.onChange(value);
|
|
||||||
if (props.cancelable && active === value) {
|
|
||||||
setActive(null);
|
|
||||||
} else {
|
|
||||||
setActive(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="flex-center gap-6">
|
|
||||||
{props.options?.map?.((option, index) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size={props.size}
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => handleChange(option.value)}
|
|
||||||
variant="filled"
|
|
||||||
color={active === option.value ? 'default' : undefined}
|
|
||||||
type={type}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(CheckButtons);
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface CollapseProps {
|
|
||||||
open: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
duration?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Collapse({
|
|
||||||
open,
|
|
||||||
children,
|
|
||||||
minHeight = 0,
|
|
||||||
duration = 200
|
|
||||||
}: CollapseProps) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [height, setHeight] = useState<number | 'auto'>(minHeight);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
if (open) {
|
|
||||||
const h = el.scrollHeight;
|
|
||||||
setHeight(h);
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setHeight('auto');
|
|
||||||
}, duration);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
} else {
|
|
||||||
const h = el.scrollHeight;
|
|
||||||
setHeight(h);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setHeight(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [open, duration]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
style={{
|
|
||||||
height,
|
|
||||||
overflow: 'hidden',
|
|
||||||
transition: `height ${duration}ms ease`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
|
||||||
import { Card } from 'antd';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const CardStyled = styled(Card)`
|
|
||||||
box-shadow: none !important;
|
|
||||||
background-color: none;
|
|
||||||
&.isOpen {
|
|
||||||
.ant-card-head {
|
|
||||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
|
||||||
border-radius: var(--ant-border-radius) var(--ant-border-radius) 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-card-head {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--ant-color-fill-quaternary);
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: var(--ant-border-radius);
|
|
||||||
padding: 0 16px;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--ant-color-fill-secondary);
|
|
||||||
.del-btn {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.disabled {
|
|
||||||
.ant-card-head {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background-color: var(--ant-color-fill-quaternary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
|
||||||
return {
|
|
||||||
title: css`
|
|
||||||
font-weight: 400;
|
|
||||||
min-height: 56px;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
`,
|
|
||||||
expandIcon: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`,
|
|
||||||
subtitle: css`
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${token.colorTextSecondary};
|
|
||||||
`,
|
|
||||||
content: css`
|
|
||||||
padding-top: 8px;
|
|
||||||
`,
|
|
||||||
left: css`
|
|
||||||
flex: 1;
|
|
||||||
`,
|
|
||||||
right: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
.del-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface CollapsibleContainerProps {
|
|
||||||
title?: React.ReactNode;
|
|
||||||
subtitle?: React.ReactNode;
|
|
||||||
right?: React.ReactNode;
|
|
||||||
deleteBtn?: React.ReactNode;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
open?: boolean;
|
|
||||||
collapsible?: boolean;
|
|
||||||
showExpandIcon?: boolean;
|
|
||||||
onToggle?: (open: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
variant?: 'outlined' | 'borderless' | undefined;
|
|
||||||
iconPlacement?: 'left' | 'right';
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
styles?: {
|
|
||||||
root?: React.CSSProperties;
|
|
||||||
body?: React.CSSProperties;
|
|
||||||
header?: React.CSSProperties;
|
|
||||||
content?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CollapsibleContainer({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
right,
|
|
||||||
deleteBtn,
|
|
||||||
defaultOpen = true,
|
|
||||||
open,
|
|
||||||
onToggle,
|
|
||||||
disabled = false,
|
|
||||||
showExpandIcon = true,
|
|
||||||
variant = 'borderless',
|
|
||||||
className = '',
|
|
||||||
collapsible,
|
|
||||||
iconPlacement = 'left',
|
|
||||||
styles: cardStyles,
|
|
||||||
children
|
|
||||||
}: CollapsibleContainerProps) {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const isControlled = typeof open === 'boolean';
|
|
||||||
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
||||||
const isOpen = collapsible
|
|
||||||
? isControlled
|
|
||||||
? (open as boolean)
|
|
||||||
: internalOpen
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
if (disabled || !collapsible) return;
|
|
||||||
const next = !isOpen;
|
|
||||||
if (!isControlled) setInternalOpen(next);
|
|
||||||
onToggle?.(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [height, setHeight] = useState(isOpen ? 'auto' : '0px');
|
|
||||||
|
|
||||||
const renderIcon = () => {
|
|
||||||
if (showExpandIcon) {
|
|
||||||
return (
|
|
||||||
<IconFont
|
|
||||||
rotate={isOpen ? 180 : 0}
|
|
||||||
type="icon-down"
|
|
||||||
style={{
|
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
||||||
fontSize: 12
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTitle = () => {
|
|
||||||
if (!collapsible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={styles.title} onClick={toggle}>
|
|
||||||
<div className={styles.left}>
|
|
||||||
<div className={styles.expandIcon}>
|
|
||||||
{iconPlacement === 'left' && renderIcon()}
|
|
||||||
{title && <div>{title}</div>}
|
|
||||||
</div>
|
|
||||||
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
|
|
||||||
</div>
|
|
||||||
<div className={styles.right}>
|
|
||||||
{right && <span>{right}</span>}
|
|
||||||
{deleteBtn && <span className="del-btn">{deleteBtn}</span>}
|
|
||||||
{iconPlacement === 'right' && renderIcon()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!collapsible) {
|
|
||||||
setHeight('auto');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isOpen) {
|
|
||||||
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
|
||||||
setHeight(scrollHeight + 'px');
|
|
||||||
const timer = setTimeout(() => setHeight('auto'), 200);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
} else {
|
|
||||||
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
|
||||||
setHeight(scrollHeight + 'px');
|
|
||||||
requestAnimationFrame(() => setHeight('0px'));
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
}, [isOpen, collapsible]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardStyled
|
|
||||||
className={classNames(className, { collapsible, disabled, isOpen })}
|
|
||||||
variant={variant}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
...cardStyles?.root
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
padding: 0,
|
|
||||||
...cardStyles?.body
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
...cardStyles?.header
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={renderTitle()}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={contentRef}
|
|
||||||
style={{
|
|
||||||
height: height,
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ paddingTop: 8, ...cardStyles?.content }}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</CardStyled>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.content-wrapper {
|
|
||||||
.content {
|
|
||||||
padding-block-start: 0;
|
|
||||||
padding-block-end: 32px;
|
|
||||||
padding-inline: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--font-size-large);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 32px;
|
|
||||||
padding-block-start: 8px;
|
|
||||||
padding-block-end: 16px;
|
|
||||||
padding-inline-start: 40px;
|
|
||||||
padding-inline-end: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
const ContentWrapper: React.FC<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
title: React.ReactNode;
|
|
||||||
titleStyle?: React.CSSProperties;
|
|
||||||
contentStyle?: React.CSSProperties;
|
|
||||||
}> = ({ children, title = false, titleStyle, contentStyle }) => {
|
|
||||||
return (
|
|
||||||
<div className="content-wrapper">
|
|
||||||
{title && (
|
|
||||||
<div className="title" style={{ ...titleStyle }}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="content" style={{ ...contentStyle }}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContentWrapper;
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, message, Tooltip } from 'antd';
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import AutoTooltip from '../auto-tooltip';
|
|
||||||
|
|
||||||
type CopyButtonProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
text: string;
|
|
||||||
fontSize?: string;
|
|
||||||
type?: 'text' | 'primary' | 'dashed' | 'link' | 'default';
|
|
||||||
size?: 'small' | 'middle' | 'large';
|
|
||||||
shape?: 'circle' | 'round' | 'default';
|
|
||||||
tips?: string;
|
|
||||||
placement?:
|
|
||||||
| 'top'
|
|
||||||
| 'left'
|
|
||||||
| 'right'
|
|
||||||
| 'bottom'
|
|
||||||
| 'topLeft'
|
|
||||||
| 'topRight'
|
|
||||||
| 'bottomLeft'
|
|
||||||
| 'bottomRight';
|
|
||||||
btnStyle?: React.CSSProperties;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CopyButton: React.FC<CopyButtonProps> = ({
|
|
||||||
children,
|
|
||||||
tips,
|
|
||||||
text,
|
|
||||||
type = 'text',
|
|
||||||
shape = 'default',
|
|
||||||
fontSize = '14px',
|
|
||||||
style,
|
|
||||||
btnStyle,
|
|
||||||
placement,
|
|
||||||
size = 'small'
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const timerRef = useRef<number>();
|
|
||||||
|
|
||||||
const resetCopied = () => {
|
|
||||||
window.clearTimeout(timerRef.current);
|
|
||||||
timerRef.current = window.setTimeout(() => {
|
|
||||||
setCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modern clipboard API (works in secure contexts: HTTPS or localhost)
|
|
||||||
*/
|
|
||||||
const asyncCopy = async (value: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(value);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback: execCommand with copy event listener
|
|
||||||
* More reliable than textarea selection method
|
|
||||||
*/
|
|
||||||
const execCopy = (value: string): boolean => {
|
|
||||||
let copySuccess = false;
|
|
||||||
|
|
||||||
const onCopy = (event: ClipboardEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
event.clipboardData?.clearData();
|
|
||||||
event.clipboardData?.setData('text/plain', value);
|
|
||||||
copySuccess = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.addEventListener('copy', onCopy, { capture: true });
|
|
||||||
document.execCommand('copy');
|
|
||||||
return copySuccess;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
document.removeEventListener('copy', onCopy, { capture: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
try {
|
|
||||||
// Try modern clipboard API first
|
|
||||||
if (await asyncCopy(text)) {
|
|
||||||
setCopied(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to execCommand method
|
|
||||||
if (execCopy(text)) {
|
|
||||||
setCopied(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both methods failed
|
|
||||||
throw new Error('Copy failed');
|
|
||||||
} catch (error) {
|
|
||||||
message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tipTitle = useMemo(() => {
|
|
||||||
if (copied) {
|
|
||||||
return intl.formatMessage({ id: 'common.button.copied' });
|
|
||||||
}
|
|
||||||
return tips ?? intl.formatMessage({ id: 'common.button.copy' });
|
|
||||||
}, [copied, tips, intl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resetCopied();
|
|
||||||
}, [copied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-center gap-4" style={{ minWidth: 16 }}>
|
|
||||||
{children && (
|
|
||||||
<AutoTooltip minWidth={20} ghost>
|
|
||||||
{children}
|
|
||||||
</AutoTooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip title={tipTitle} placement={placement}>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
className="copy-button"
|
|
||||||
type={type}
|
|
||||||
shape={shape}
|
|
||||||
size={size}
|
|
||||||
onClick={handleCopy}
|
|
||||||
style={{ ...btnStyle }}
|
|
||||||
icon={
|
|
||||||
copied ? (
|
|
||||||
<CheckCircleFilled
|
|
||||||
style={{
|
|
||||||
color: 'var(--ant-color-success)',
|
|
||||||
fontSize
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CopyOutlined style={{ fontSize, ...style }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CopyButton;
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
message,
|
|
||||||
type ModalFuncProps
|
|
||||||
} from 'antd';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ css }) => ({
|
|
||||||
'delete-modal-content': css`
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--font-size-middle);
|
|
||||||
.anticon {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--ant-color-warning);
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: var(--font-weight-500);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
content: css`
|
|
||||||
padding-top: 15px;
|
|
||||||
padding-left: 30px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
white-space: pre-line;
|
|
||||||
word-break: break-all;
|
|
||||||
span {
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
display: flex;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}));
|
|
||||||
|
|
||||||
const CheckboxWrapper = styled.div`
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-left: 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
.check-text {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--ant-color-warning);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface DataOptions {
|
|
||||||
content?: string;
|
|
||||||
selection?: boolean;
|
|
||||||
name?: string;
|
|
||||||
okText?: string;
|
|
||||||
cancelText?: string;
|
|
||||||
title?: string;
|
|
||||||
operation: string;
|
|
||||||
checkConfig?: {
|
|
||||||
checkText: string;
|
|
||||||
defautlChecked: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Configuration {
|
|
||||||
checked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// default need to pass content and operation
|
|
||||||
const DeleteModal = forwardRef((props, ref) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [configuration, setConfiguration] = useState<Configuration>({
|
|
||||||
checked: false
|
|
||||||
});
|
|
||||||
const [delLoading, setDelLoading] = useState(false);
|
|
||||||
const [config, setConfig] = useState<ModalFuncProps & DataOptions>({} as any);
|
|
||||||
|
|
||||||
const show = (data: ModalFuncProps & DataOptions) => {
|
|
||||||
saveScrollHeight();
|
|
||||||
setConfig(data);
|
|
||||||
setConfiguration({
|
|
||||||
checked: data.checkConfig?.defautlChecked || false
|
|
||||||
});
|
|
||||||
setVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hide = () => {
|
|
||||||
setVisible(false);
|
|
||||||
restoreScrollHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setVisible(false);
|
|
||||||
config.onCancel?.();
|
|
||||||
restoreScrollHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOk = async () => {
|
|
||||||
try {
|
|
||||||
setDelLoading(true);
|
|
||||||
const res = await config.onOk?.();
|
|
||||||
const isArray = Array.isArray(res);
|
|
||||||
if (isArray) {
|
|
||||||
const allSuccess = res.every(
|
|
||||||
(item: any) => item?.status === 'fulfilled'
|
|
||||||
);
|
|
||||||
if (allSuccess) {
|
|
||||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error if needed
|
|
||||||
} finally {
|
|
||||||
setVisible(false);
|
|
||||||
setDelLoading(false);
|
|
||||||
restoreScrollHeight();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
show,
|
|
||||||
hide,
|
|
||||||
configuration
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
style={{
|
|
||||||
top: '20%'
|
|
||||||
}}
|
|
||||||
open={visible}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
destroyOnHidden={false}
|
|
||||||
closeIcon={false}
|
|
||||||
mask={{
|
|
||||||
closable: false
|
|
||||||
}}
|
|
||||||
keyboard={false}
|
|
||||||
width={460}
|
|
||||||
styles={{
|
|
||||||
footer: {
|
|
||||||
marginTop: '20px'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
footer={
|
|
||||||
<Space size={20}>
|
|
||||||
<Button onClick={handleCancel} size="middle">
|
|
||||||
{config.cancelText
|
|
||||||
? intl.formatMessage({ id: config.cancelText })
|
|
||||||
: intl.formatMessage({ id: 'common.button.cancel' })}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleOk}
|
|
||||||
size="middle"
|
|
||||||
danger
|
|
||||||
loading={delLoading}
|
|
||||||
>
|
|
||||||
{config.okText
|
|
||||||
? intl.formatMessage({ id: config.okText })
|
|
||||||
: intl.formatMessage({ id: 'common.button.delete' })}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles['delete-modal-content']}>
|
|
||||||
<span className="title">
|
|
||||||
<ExclamationCircleFilled />
|
|
||||||
<span>
|
|
||||||
{config.title
|
|
||||||
? intl.formatMessage({ id: config.title })
|
|
||||||
: intl.formatMessage({ id: 'common.title.delete.confirm' })}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={styles['content']}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: config.content
|
|
||||||
? intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: config.operation || ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: intl.formatMessage({ id: config.content }),
|
|
||||||
name: config.name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
{config.checkConfig && (
|
|
||||||
<CheckboxWrapper>
|
|
||||||
<Checkbox
|
|
||||||
checked={configuration.checked}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConfiguration({
|
|
||||||
checked: e.target.checked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="check-text">
|
|
||||||
{intl.formatMessage({ id: config.checkConfig?.checkText })}
|
|
||||||
</span>
|
|
||||||
</Checkbox>
|
|
||||||
</CheckboxWrapper>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DeleteModal;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.divider-line {
|
|
||||||
height: 8px;
|
|
||||||
// border-radius: 4px;
|
|
||||||
width: 100%;
|
|
||||||
// background-color: var(--color-fill-1);
|
|
||||||
z-index: 100;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -9px;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--color-fill-1);
|
|
||||||
// border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import styles from './index.less';
|
|
||||||
const DividerLine: React.FC = () => {
|
|
||||||
return <div className={styles['divider-line']}></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DividerLine;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Dropdown, DropDownProps } from 'antd';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
|
|
||||||
const DropDownActions: React.FC<DropDownProps> = (props) => {
|
|
||||||
const {
|
|
||||||
menu,
|
|
||||||
trigger = ['hover'],
|
|
||||||
placement = 'bottomRight',
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
|
||||||
return menu?.items?.map((item: any) => ({
|
|
||||||
..._.omit(item, 'locale'),
|
|
||||||
icon: item.icon
|
|
||||||
? React.cloneElement(item.icon, { style: { fontSize: 14 } })
|
|
||||||
: null,
|
|
||||||
label: item.locale ? intl.formatMessage({ id: item.label }) : item.label
|
|
||||||
}));
|
|
||||||
}, [menu?.items, intl]);
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: items,
|
|
||||||
onClick: menu?.onClick
|
|
||||||
}}
|
|
||||||
trigger={trigger}
|
|
||||||
placement={placement}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropDownActions;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.dropdown-button.middle {
|
|
||||||
height: 28px;
|
|
||||||
width: 28px;
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { MoreOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Dropdown, Space, Tooltip, type MenuProps } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
type Trigger = 'click' | 'hover';
|
|
||||||
interface DropdownButtonsProps {
|
|
||||||
items: MenuProps['items'];
|
|
||||||
size?: 'small' | 'middle' | 'large';
|
|
||||||
trigger?: Trigger[];
|
|
||||||
showText?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
variant?: 'filled' | 'outlined';
|
|
||||||
color?: string;
|
|
||||||
extra?: React.ReactNode;
|
|
||||||
onSelect: (val: any, item?: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--ant-color-bg-elevated);
|
|
||||||
padding: 5px;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
box-shadow: var(--ant-box-shadow-secondary);
|
|
||||||
min-width: 160px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DropdownButtons: React.FC<
|
|
||||||
DropdownButtonsProps & { items: MenuProps['items'] }
|
|
||||||
> = ({
|
|
||||||
items,
|
|
||||||
size = 'middle',
|
|
||||||
trigger = ['hover'],
|
|
||||||
showText,
|
|
||||||
disabled,
|
|
||||||
variant,
|
|
||||||
color,
|
|
||||||
extra,
|
|
||||||
onSelect
|
|
||||||
}) => {
|
|
||||||
const headItem = _.head(items);
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const handleMenuClick = (item: any) => {
|
|
||||||
const selectItem = _.find(items, { key: item.key });
|
|
||||||
onSelect(item.key, selectItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleButtonClick = (e: any) => {
|
|
||||||
const headItem = _.head(items);
|
|
||||||
onSelect(headItem.key, headItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!items?.length) {
|
|
||||||
return <span></span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{items?.length === 1 ? (
|
|
||||||
<Tooltip title={intl.formatMessage({ id: headItem?.label })}>
|
|
||||||
<Button
|
|
||||||
className={classNames('dropdown-button', size)}
|
|
||||||
icon={headItem?.icon}
|
|
||||||
size={size}
|
|
||||||
{...headItem?.props}
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
></Button>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Space.Compact>
|
|
||||||
<>
|
|
||||||
{showText ? (
|
|
||||||
<Button
|
|
||||||
{...headItem?.props}
|
|
||||||
disabled={headItem?.disabled || disabled}
|
|
||||||
className={classNames('dropdown-button', size)}
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
size={size}
|
|
||||||
icon={headItem?.icon}
|
|
||||||
variant={variant}
|
|
||||||
color={color}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: headItem?.label
|
|
||||||
})}
|
|
||||||
{extra}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: headItem?.label })}
|
|
||||||
key="leftButton"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
{...headItem?.props}
|
|
||||||
className={classNames('dropdown-button', size)}
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
size={size}
|
|
||||||
icon={headItem?.icon}
|
|
||||||
disabled={headItem?.disabled}
|
|
||||||
></Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
<Dropdown
|
|
||||||
disabled={disabled}
|
|
||||||
trigger={trigger}
|
|
||||||
placement="bottomRight"
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
minWidth: 160
|
|
||||||
},
|
|
||||||
itemIcon: {
|
|
||||||
fontSize: 14
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
menu={{
|
|
||||||
onClick: handleMenuClick,
|
|
||||||
items: _.tail(items).map((item: any) => ({
|
|
||||||
..._.omit(item, ['label', 'locale']),
|
|
||||||
...item.props,
|
|
||||||
label:
|
|
||||||
item.locale || item.locale === undefined
|
|
||||||
? intl.formatMessage({ id: item.label })
|
|
||||||
: item.label
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={<MoreOutlined />}
|
|
||||||
size={size}
|
|
||||||
key="menu"
|
|
||||||
variant={variant}
|
|
||||||
color="default"
|
|
||||||
className={classNames('dropdown-button', size)}
|
|
||||||
></Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space.Compact>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropdownButtons;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import ComponentsMap from '@/components/seal-form/config/components';
|
|
||||||
import { SealFormItemProps } from '@/components/seal-form/types';
|
|
||||||
import { Form } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface FieldItemProps extends SealFormItemProps {
|
|
||||||
widget: keyof typeof ComponentsMap;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FieldItem: React.FC<FieldItemProps> = (props) => {
|
|
||||||
const { name, widget, required = [], ...rest } = props;
|
|
||||||
|
|
||||||
const Component = ComponentsMap[widget];
|
|
||||||
|
|
||||||
return <Form.Item name={name}></Form.Item>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldItem;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import ComponentsMap from '@/components/seal-form/config/components';
|
|
||||||
import { FormWidgetProps } from '../config/types';
|
|
||||||
|
|
||||||
const FormWidget: React.FC<
|
|
||||||
FormWidgetProps & {
|
|
||||||
onChange?: (data: any) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
> = ({
|
|
||||||
widget,
|
|
||||||
title: label,
|
|
||||||
required,
|
|
||||||
placeholder,
|
|
||||||
options,
|
|
||||||
description,
|
|
||||||
enum: enumValues,
|
|
||||||
style,
|
|
||||||
value,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
status,
|
|
||||||
checked,
|
|
||||||
isInFormItems,
|
|
||||||
disabled,
|
|
||||||
onChange
|
|
||||||
}) => {
|
|
||||||
const Component = ComponentsMap[widget];
|
|
||||||
|
|
||||||
const optionList = enumValues?.map((item: string | number) => ({
|
|
||||||
label: item,
|
|
||||||
value: item
|
|
||||||
}));
|
|
||||||
|
|
||||||
return Component ? (
|
|
||||||
<Component
|
|
||||||
{...{
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
placeholder,
|
|
||||||
description,
|
|
||||||
min,
|
|
||||||
max
|
|
||||||
}}
|
|
||||||
status={status}
|
|
||||||
isInFormItems={isInFormItems}
|
|
||||||
disabled={disabled}
|
|
||||||
options={options || optionList}
|
|
||||||
value={value}
|
|
||||||
checked={checked}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
...style
|
|
||||||
}}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormWidget;
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import Wrapper from '@/components/label-selector/wrapper';
|
|
||||||
import { MinusOutlined } from '@ant-design/icons';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import React, { useEffect, useMemo } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { statusType } from '../config/types';
|
|
||||||
import FormWidget from './form-widget';
|
|
||||||
|
|
||||||
const RowWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const WidgetBox = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
interface ListMapProps {
|
|
||||||
minItems?: number;
|
|
||||||
dataList: any[];
|
|
||||||
label?: React.ReactNode;
|
|
||||||
btnText?: string;
|
|
||||||
requiredFields?: string[];
|
|
||||||
validateStatusList?: Record<string, statusType>[];
|
|
||||||
properties: Record<string, any>;
|
|
||||||
disabled?: boolean;
|
|
||||||
onAdd?: (data: any[]) => void;
|
|
||||||
onDelete?: (deletedItem: any, data: any[]) => void;
|
|
||||||
onChange?: (data: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListItemProps {
|
|
||||||
schemaList: any[];
|
|
||||||
data: Record<string, any>;
|
|
||||||
disabled?: boolean;
|
|
||||||
validateStatus?: Record<string, statusType>;
|
|
||||||
onChange?: (data: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListItem: React.FC<ListItemProps> = ({
|
|
||||||
schemaList,
|
|
||||||
data,
|
|
||||||
onChange,
|
|
||||||
validateStatus,
|
|
||||||
disabled
|
|
||||||
}) => {
|
|
||||||
const handleValueChange = (name: string, target: any) => {
|
|
||||||
if (target?.target?.type === 'checkbox') {
|
|
||||||
const checked = target.target?.checked;
|
|
||||||
onChange?.({ [name]: checked });
|
|
||||||
} else {
|
|
||||||
const value = target?.target ? target.target.value : target;
|
|
||||||
onChange?.({ [name]: value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{schemaList.map((schema: any) => (
|
|
||||||
<FormWidget
|
|
||||||
status={validateStatus?.[schema.name]}
|
|
||||||
widget={schema.type}
|
|
||||||
{...schema}
|
|
||||||
disabled={disabled || schema.readOnly}
|
|
||||||
key={schema.name}
|
|
||||||
value={data?.[schema.name]}
|
|
||||||
checked={data?.[schema.name]}
|
|
||||||
isInFormItems={false}
|
|
||||||
onChange={(target) => handleValueChange(schema.name, target)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ListMap: React.FC<ListMapProps> = ({
|
|
||||||
dataList = [],
|
|
||||||
label,
|
|
||||||
btnText,
|
|
||||||
properties = {},
|
|
||||||
requiredFields = [],
|
|
||||||
minItems = 0,
|
|
||||||
validateStatusList = [],
|
|
||||||
disabled,
|
|
||||||
onAdd,
|
|
||||||
onDelete,
|
|
||||||
onChange
|
|
||||||
}) => {
|
|
||||||
const [items, setItems] = React.useState(dataList || []);
|
|
||||||
|
|
||||||
const schemaList = useMemo(() => {
|
|
||||||
const list = Object.entries(properties).map(([key, value]) => ({
|
|
||||||
...value,
|
|
||||||
required: requiredFields.includes(key),
|
|
||||||
name: key
|
|
||||||
}));
|
|
||||||
return list;
|
|
||||||
}, [properties, requiredFields]);
|
|
||||||
|
|
||||||
const handleOnAdd = () => {
|
|
||||||
const keys = Object.keys(properties);
|
|
||||||
const newItems = [
|
|
||||||
...items,
|
|
||||||
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
|
|
||||||
];
|
|
||||||
setItems(newItems);
|
|
||||||
onAdd?.(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (index: number) => {
|
|
||||||
const deleteItem = items[index];
|
|
||||||
const newItems = items.filter((_, i) => i !== index);
|
|
||||||
setItems(newItems);
|
|
||||||
onDelete?.(deleteItem, newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleItemChange = (index: number, data: { [key: string]: any }) => {
|
|
||||||
const newItems = [...items];
|
|
||||||
newItems[index] = { ...newItems[index], ...data };
|
|
||||||
setItems(newItems);
|
|
||||||
onChange?.(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dataList.length && minItems > 0) {
|
|
||||||
handleOnAdd();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setItems(dataList);
|
|
||||||
}, [dataList]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
label={label}
|
|
||||||
btnText={btnText}
|
|
||||||
onAdd={handleOnAdd}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<RowWrapper key={index}>
|
|
||||||
<WidgetBox>
|
|
||||||
<ListItem
|
|
||||||
schemaList={schemaList}
|
|
||||||
data={item}
|
|
||||||
validateStatus={validateStatusList?.[index]}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(value) => handleItemChange(index, value)}
|
|
||||||
/>
|
|
||||||
</WidgetBox>
|
|
||||||
{!disabled && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="default"
|
|
||||||
shape="circle"
|
|
||||||
style={{
|
|
||||||
width: 24,
|
|
||||||
marginLeft: 10,
|
|
||||||
flex: 'none'
|
|
||||||
}}
|
|
||||||
icon={<MinusOutlined />}
|
|
||||||
onClick={() => handleDelete(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</RowWrapper>
|
|
||||||
))}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListMap;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
// refer to json schema
|
|
||||||
export interface FieldSchema {
|
|
||||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
|
||||||
title?: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
properties?: Record<string, FieldSchema>;
|
|
||||||
default?: any;
|
|
||||||
enum?: string[];
|
|
||||||
minItems?: number;
|
|
||||||
maxItems?: number;
|
|
||||||
items?: FieldSchema[];
|
|
||||||
widget?: string;
|
|
||||||
min?: number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
required?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type statusType = 'error' | 'warning' | '' | undefined;
|
|
||||||
export interface FormWidgetProps {
|
|
||||||
status?: statusType;
|
|
||||||
isInFormItems?: boolean;
|
|
||||||
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
|
|
||||||
name: string;
|
|
||||||
title?: string;
|
|
||||||
required?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
readOnly?: boolean;
|
|
||||||
options?: { label: string; value: string | number }[];
|
|
||||||
description?: string;
|
|
||||||
enum?: (string | number)[];
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
value?: any;
|
|
||||||
checked?: boolean;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { FieldSchema } from '../config/types';
|
|
||||||
|
|
||||||
interface ParsedField {
|
|
||||||
name: (string | number)[];
|
|
||||||
schema: FieldSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseSchema = (
|
|
||||||
schema: Record<string, FieldSchema>,
|
|
||||||
parentName: (string | number)[] = []
|
|
||||||
): ParsedField[] => {
|
|
||||||
const fields: ParsedField[] = [];
|
|
||||||
|
|
||||||
Object.entries(schema).forEach(([key, fieldSchema]) => {
|
|
||||||
const currentName = [...parentName, key];
|
|
||||||
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
|
||||||
fields.push(...parseSchema(fieldSchema.properties, currentName));
|
|
||||||
} else if (fieldSchema.type === 'array' && fieldSchema.items) {
|
|
||||||
fields.push({ name: currentName, schema: fieldSchema });
|
|
||||||
} else {
|
|
||||||
fields.push({ name: currentName, schema: fieldSchema });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useParsedFields = (schema: Record<string, FieldSchema>) => {
|
|
||||||
return useMemo(() => parseSchema(schema), [schema]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useParsedFields;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { useRef } from 'react';
|
|
||||||
import { statusType } from '../config/types';
|
|
||||||
|
|
||||||
export default function useValidateFields(params: {
|
|
||||||
requiredFields?: string[];
|
|
||||||
setValidateStatusList: (statusList: { [key: string]: statusType }[]) => void;
|
|
||||||
}) {
|
|
||||||
const { requiredFields, setValidateStatusList } = params;
|
|
||||||
const validationEnabled = useRef(false);
|
|
||||||
|
|
||||||
const isEmptyValue = (value: any, key: string) => {
|
|
||||||
return !value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateRule = (value: any, key: string) => {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const listMapValidator = async (_: any, valueList: any) => {
|
|
||||||
if (!validationEnabled.current) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = new Set<string>();
|
|
||||||
const statusList: { [key: string]: statusType }[] = [];
|
|
||||||
|
|
||||||
(valueList || []).forEach((item: any, index: number) => {
|
|
||||||
const status: { [key: string]: statusType } = {};
|
|
||||||
Object.entries(item || {}).forEach(([key, value]) => {
|
|
||||||
if (isEmptyValue(value, key)) {
|
|
||||||
fields.add(key);
|
|
||||||
if (requiredFields?.includes(key)) {
|
|
||||||
status[key] = 'error';
|
|
||||||
} else {
|
|
||||||
status[key] = '';
|
|
||||||
}
|
|
||||||
} else if (validateRule(value, key)) {
|
|
||||||
status[key] = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
statusList.push(status);
|
|
||||||
});
|
|
||||||
|
|
||||||
setValidateStatusList(statusList);
|
|
||||||
|
|
||||||
if (fields.size > 0) {
|
|
||||||
return Promise.reject(`${Array.from(fields).join(', ')} is required`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleValidation = (enabled: boolean) => {
|
|
||||||
validationEnabled.current = enabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
listMapValidator,
|
|
||||||
toggleValidation
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Form } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { FieldSchema } from './config/types';
|
|
||||||
|
|
||||||
interface DynamicFormProps {
|
|
||||||
schema: FieldSchema;
|
|
||||||
onSubmit: (values: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit }) => {
|
|
||||||
const form = Form.useFormInstance();
|
|
||||||
|
|
||||||
const handleFinish = (values: any) => {
|
|
||||||
onSubmit(values);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form form={form} onFinish={handleFinish}>
|
|
||||||
{/* Render form fields based on schema */}
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DynamicForm;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { memo, useMemo } from 'react';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const BarChart: React.FC<ChartProps> = (props) => {
|
|
||||||
const {
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
labelFormatter,
|
|
||||||
legendData,
|
|
||||||
title
|
|
||||||
} = props;
|
|
||||||
const {
|
|
||||||
barItemConfig,
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
title: titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} = useChartConfig();
|
|
||||||
|
|
||||||
const dataOptions = useMemo((): any => {
|
|
||||||
const options = {
|
|
||||||
title: {
|
|
||||||
text: ''
|
|
||||||
},
|
|
||||||
grid,
|
|
||||||
tooltip: {
|
|
||||||
...tooltip
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...xAxis,
|
|
||||||
axisLabel: {
|
|
||||||
...xAxis.axisLabel,
|
|
||||||
formatter: labelFormatter
|
|
||||||
},
|
|
||||||
data: []
|
|
||||||
},
|
|
||||||
yAxis,
|
|
||||||
legend: {
|
|
||||||
...legend,
|
|
||||||
data: []
|
|
||||||
},
|
|
||||||
|
|
||||||
series: []
|
|
||||||
};
|
|
||||||
const data = _.map(seriesData, (item: any) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...barItemConfig,
|
|
||||||
stack: 'total',
|
|
||||||
itemStyle: {
|
|
||||||
color: item.color
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
animation: false,
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
text: title
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...options.yAxis
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...options.xAxis,
|
|
||||||
data: xAxisData
|
|
||||||
},
|
|
||||||
series: data
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
title,
|
|
||||||
labelFormatter,
|
|
||||||
tooltip,
|
|
||||||
grid,
|
|
||||||
xAxis,
|
|
||||||
yAxis,
|
|
||||||
legend,
|
|
||||||
barItemConfig
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!seriesData.length ? (
|
|
||||||
<EmptyData height={height} title={title}></EmptyData>
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
height={height}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(BarChart);
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const TooltipWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
background-color: rgba(255, 255, 255, 80%);
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: 360px;
|
|
||||||
|
|
||||||
.tooltip-x-name {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-item {
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.tooltip-item-title {
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-value {
|
|
||||||
margin-left: 10px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ItemSymbol = styled.span<{ $color: string }>`
|
|
||||||
background-color: ${(props) => props.$color};
|
|
||||||
display: inline-block;
|
|
||||||
marginright: 5px;
|
|
||||||
borderradius: 8px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ChartTooltipProps {
|
|
||||||
params: any[];
|
|
||||||
callback?: (val: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip: React.FC<ChartTooltipProps> = (props) => {
|
|
||||||
const { params, callback } = props;
|
|
||||||
console.log('params====', params);
|
|
||||||
return (
|
|
||||||
<TooltipWrapper>
|
|
||||||
<span className="tooltip-x-name">{params[0]?.axisValue}</span>
|
|
||||||
<>
|
|
||||||
{params.map((item: any, index: number) => {
|
|
||||||
let value = callback?.(item.data.value) || item.data.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="tooltip-item" key={index}>
|
|
||||||
<span className="tooltip-item-name">
|
|
||||||
<ItemSymbol $color={item.color}></ItemSymbol>
|
|
||||||
<span className="tooltip-title">{item.seriesName}</span>:
|
|
||||||
</span>
|
|
||||||
<span className="tooltip-value">{value}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</TooltipWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default ChartTooltip;
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import _, { throttle } from 'lodash';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef
|
|
||||||
} from 'react';
|
|
||||||
import echarts, { ECOption } from '.';
|
|
||||||
|
|
||||||
const Chart: React.FC<{
|
|
||||||
options: ECOption;
|
|
||||||
chartHeight?: number;
|
|
||||||
height: number | string;
|
|
||||||
width: number | string;
|
|
||||||
ref?: any;
|
|
||||||
}> = forwardRef(({ options, width, height, chartHeight }, ref) => {
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
|
||||||
const chart = useRef<echarts.EChartsType>();
|
|
||||||
const resizeable = useRef(false);
|
|
||||||
const resizeObserver = useRef<ResizeObserver>();
|
|
||||||
const finished = useRef(false);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => {
|
|
||||||
return {
|
|
||||||
chart: chart.current
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
if (container.current) {
|
|
||||||
chart.current?.clear();
|
|
||||||
chart.current = echarts.init(container.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setOption = (options: ECOption) => {
|
|
||||||
if (!chart.current) return;
|
|
||||||
chart.current?.clear();
|
|
||||||
chart.current?.setOption(options, {
|
|
||||||
notMerge: true,
|
|
||||||
lazyUpdate: true
|
|
||||||
});
|
|
||||||
if (Array.isArray(options.yAxis) && options.yAxis.length > 1) {
|
|
||||||
chart.current?.resize();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleOnFinished = () => {
|
|
||||||
if (!chart.current || finished.current) return;
|
|
||||||
|
|
||||||
const currentChart = chart.current;
|
|
||||||
const optionsYAxis = currentChart.getOption()?.yAxis;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!optionsYAxis ||
|
|
||||||
!Array.isArray(optionsYAxis) ||
|
|
||||||
optionsYAxis.length < 2
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
// @ts-ignore
|
|
||||||
const model = currentChart.getModel();
|
|
||||||
|
|
||||||
const yAxisModels = [
|
|
||||||
model.getComponent('yAxis', 0),
|
|
||||||
model.getComponent('yAxis', 1)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!yAxisModels[0] || !yAxisModels[1]) return;
|
|
||||||
|
|
||||||
const axes = yAxisModels.map((m) => m.axis);
|
|
||||||
|
|
||||||
const intervals = axes.map((axis) => axis.scale.getInterval());
|
|
||||||
const ticksList = axes.map((axis) => axis.scale.getTicks());
|
|
||||||
const counts = ticksList.map((t) => t.length);
|
|
||||||
|
|
||||||
const unifiedCount = Math.max(counts[0], counts[1]);
|
|
||||||
|
|
||||||
const newMax0 = intervals[0] * (unifiedCount - 1);
|
|
||||||
const newMax1 = intervals[1] * (unifiedCount - 1);
|
|
||||||
|
|
||||||
// if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis
|
|
||||||
if (counts[0] === counts[1]) return;
|
|
||||||
|
|
||||||
const yAxis: any[] = [{}, {}];
|
|
||||||
|
|
||||||
if (counts[0] < unifiedCount) {
|
|
||||||
yAxis[0].max = _.round(newMax0, 2);
|
|
||||||
yAxis[0].interval = intervals[0];
|
|
||||||
yAxis[0].splitNumber = unifiedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counts[1] < unifiedCount) {
|
|
||||||
yAxis[1].max = _.round(newMax1, 2);
|
|
||||||
yAxis[1].interval = intervals[1];
|
|
||||||
yAxis[1].splitNumber = unifiedCount;
|
|
||||||
}
|
|
||||||
finished.current = true;
|
|
||||||
|
|
||||||
currentChart.setOption({
|
|
||||||
yAxis: yAxis
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (container.current) {
|
|
||||||
init();
|
|
||||||
chart.current?.on('finished', handleOnFinished);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
chart.current?.off('finished', handleOnFinished);
|
|
||||||
chart.current?.dispose();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resizeable.current = false;
|
|
||||||
finished.current = false;
|
|
||||||
setOption(options);
|
|
||||||
resizeable.current = true;
|
|
||||||
}, [options]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = throttle(() => {
|
|
||||||
if (resizeable.current) {
|
|
||||||
chart.current?.resize();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
if (container.current) {
|
|
||||||
resizeObserver.current = new ResizeObserver(handleResize);
|
|
||||||
resizeObserver.current.observe(container.current);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
resizeObserver.current?.disconnect();
|
|
||||||
resizeObserver.current = undefined;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="chart-wrapper" style={{ width: width, height }}>
|
|
||||||
<div
|
|
||||||
ref={container}
|
|
||||||
style={{ width: width, height: chartHeight || height }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Chart;
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import useUserSettings from '@/hooks/use-user-settings';
|
|
||||||
import { formatLargeNumber } from '@/utils';
|
|
||||||
import { theme } from 'antd';
|
|
||||||
import { isFunction } from 'lodash';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const grid = {
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 20,
|
|
||||||
containLabel: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useChartConfig() {
|
|
||||||
const { userSettings, isDarkTheme } = useUserSettings();
|
|
||||||
const { useToken } = theme;
|
|
||||||
const { token } = useToken();
|
|
||||||
|
|
||||||
const chartColorMap = useMemo(() => {
|
|
||||||
return {
|
|
||||||
titleColor: token.colorText,
|
|
||||||
splitLineColor: token.colorBorder,
|
|
||||||
tickLineColor: token.colorSplit,
|
|
||||||
axislabelColor: token.colorTextTertiary,
|
|
||||||
colorSecondary: token.colorTextSecondary,
|
|
||||||
colorTertiary: token.colorTextTertiary,
|
|
||||||
gaugeBgColor: token.colorFillSecondary,
|
|
||||||
gaugeSplitLineColor: isDarkTheme
|
|
||||||
? 'rgba(255,255,255,.3)'
|
|
||||||
: 'rgba(255, 255, 255, 1)',
|
|
||||||
gaugeSplitLineColor2: isDarkTheme
|
|
||||||
? 'rgba(255,255,255,.5)'
|
|
||||||
: 'rgba(255, 255, 255, 1)',
|
|
||||||
colorBgContainerHover: isDarkTheme ? '#424242' : '#fff'
|
|
||||||
};
|
|
||||||
}, [userSettings.theme, isDarkTheme]);
|
|
||||||
|
|
||||||
const tooltip = {
|
|
||||||
trigger: 'axis',
|
|
||||||
backgroundColor: chartColorMap.colorBgContainerHover,
|
|
||||||
borderColor: 'transparent',
|
|
||||||
formatter(params: any, callback?: (val: any) => any) {
|
|
||||||
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
|
||||||
let visibleItemCount = 0;
|
|
||||||
|
|
||||||
params.forEach((item: any) => {
|
|
||||||
let value = isFunction(callback)
|
|
||||||
? callback?.(item.data.value)
|
|
||||||
: item.data?.value;
|
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleItemCount += 1;
|
|
||||||
const borderRadius = item.seriesType === 'bar' ? '2px' : '8px';
|
|
||||||
|
|
||||||
result += `<span class="tooltip-item">
|
|
||||||
<span class="tooltip-item-name">
|
|
||||||
<span class="tooltip-item-dot" style="border-radius:${borderRadius};background-color:${item.color};"></span>
|
|
||||||
<span class="tooltip-item-title">${item.seriesName}</span>:
|
|
||||||
</span>
|
|
||||||
<span class="tooltip-value">${value}</span>
|
|
||||||
</span>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapperClassName =
|
|
||||||
visibleItemCount >= 12
|
|
||||||
? 'tooltip-wrapper tooltip-grid'
|
|
||||||
: 'tooltip-wrapper';
|
|
||||||
|
|
||||||
return `<div class="${wrapperClassName}">${result}</div>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const legend = {
|
|
||||||
itemWidth: 8,
|
|
||||||
itemHeight: 8,
|
|
||||||
itemGap: 12,
|
|
||||||
textStyle: {
|
|
||||||
color: chartColorMap.axislabelColor
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const xAxis = {
|
|
||||||
type: 'category',
|
|
||||||
axisTick: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: chartColorMap.tickLineColor
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: chartColorMap.axislabelColor,
|
|
||||||
fontSize: 12
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const yAxis = {
|
|
||||||
nameTextStyle: {
|
|
||||||
padding: [0, 0, 0, -20]
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: chartColorMap.splitLineColor
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: chartColorMap.axislabelColor,
|
|
||||||
fontSize: 12,
|
|
||||||
formatter: formatLargeNumber
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
type: 'value'
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = {
|
|
||||||
show: true,
|
|
||||||
left: 'center',
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: chartColorMap.titleColor
|
|
||||||
},
|
|
||||||
text: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const barItemConfig = {
|
|
||||||
type: 'bar',
|
|
||||||
barMaxWidth: 20,
|
|
||||||
barMinWidth: 8,
|
|
||||||
barGap: '30%',
|
|
||||||
barCategoryGap: '50%'
|
|
||||||
};
|
|
||||||
|
|
||||||
const lineItemConfig = {
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
showSymbol: false,
|
|
||||||
itemStyle: {},
|
|
||||||
lineStyle: {
|
|
||||||
width: 1.5,
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const gaugeItemConfig = {
|
|
||||||
type: 'gauge',
|
|
||||||
radius: '88%',
|
|
||||||
center: ['50%', '65%'],
|
|
||||||
startAngle: 190,
|
|
||||||
endAngle: -10,
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
splitNumber: 5,
|
|
||||||
progress: {
|
|
||||||
show: true,
|
|
||||||
roundCap: false,
|
|
||||||
width: 12
|
|
||||||
},
|
|
||||||
pointer: {
|
|
||||||
length: '80%',
|
|
||||||
width: 4,
|
|
||||||
itemStyle: {
|
|
||||||
color: 'auto'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
roundCap: false,
|
|
||||||
lineStyle: {
|
|
||||||
width: 12,
|
|
||||||
color: [
|
|
||||||
[0.5, 'rgba(84, 204, 152, 80%)'],
|
|
||||||
[0.8, 'rgba(250, 173, 20, 80%)'],
|
|
||||||
[1, 'rgba(255, 77, 79, 80%)']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
distance: -11,
|
|
||||||
length: 6,
|
|
||||||
splitNumber: 5,
|
|
||||||
lineStyle: {
|
|
||||||
width: 1.5,
|
|
||||||
color: chartColorMap.gaugeSplitLineColor
|
|
||||||
}
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
distance: -5,
|
|
||||||
length: 5,
|
|
||||||
lineStyle: {
|
|
||||||
width: 1.5,
|
|
||||||
color: chartColorMap.gaugeSplitLineColor2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
distance: 14,
|
|
||||||
color: chartColorMap.axislabelColor,
|
|
||||||
fontSize: 12
|
|
||||||
},
|
|
||||||
detail: {
|
|
||||||
lineHeight: 40,
|
|
||||||
height: 40,
|
|
||||||
offsetCenter: [5, 30],
|
|
||||||
valueAnimation: false,
|
|
||||||
fontSize: 20,
|
|
||||||
color: chartColorMap.titleColor,
|
|
||||||
formatter(value: any) {
|
|
||||||
return '{value|' + value + '}{unit|%}';
|
|
||||||
},
|
|
||||||
rich: {
|
|
||||||
value: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: chartColorMap.titleColor
|
|
||||||
},
|
|
||||||
unit: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: chartColorMap.titleColor,
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: [0, 0, 0, 2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
token,
|
|
||||||
tooltip,
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
xAxis,
|
|
||||||
yAxis,
|
|
||||||
title,
|
|
||||||
chartColorMap,
|
|
||||||
barItemConfig,
|
|
||||||
lineItemConfig,
|
|
||||||
gaugeItemConfig,
|
|
||||||
isDark: isDarkTheme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import React from 'react';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const strokeColorFunc = (percent: number) => {
|
|
||||||
if (percent <= 50 || percent === undefined) {
|
|
||||||
return 'rgb(84, 204, 152, 80%)';
|
|
||||||
}
|
|
||||||
if (percent <= 80) {
|
|
||||||
return 'rgba(250, 173, 20, 80%)';
|
|
||||||
}
|
|
||||||
return 'rgba(255, 77, 79, 80%)';
|
|
||||||
};
|
|
||||||
|
|
||||||
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
|
||||||
props
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
gaugeItemConfig,
|
|
||||||
title: titleConfig,
|
|
||||||
chartColorMap
|
|
||||||
} = useChartConfig();
|
|
||||||
const { value, height, width, labelFormatter, title, color, gaugeConfig } =
|
|
||||||
props;
|
|
||||||
const titleText = typeof title === 'string' ? title : title?.text;
|
|
||||||
|
|
||||||
if (!value && value !== 0) {
|
|
||||||
return <EmptyData height={height} title={titleText}></EmptyData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDataOptions = () => {
|
|
||||||
const colorValue = color || strokeColorFunc(value);
|
|
||||||
const combineGaugeConfig = {
|
|
||||||
...gaugeItemConfig,
|
|
||||||
...gaugeConfig
|
|
||||||
};
|
|
||||||
|
|
||||||
combineGaugeConfig.detail.rich.value.color = colorValue;
|
|
||||||
combineGaugeConfig.detail.rich.unit.color = colorValue;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
text: titleText,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: chartColorMap.colorSecondary,
|
|
||||||
fontWeight: 400
|
|
||||||
},
|
|
||||||
top: 10,
|
|
||||||
left: 'center',
|
|
||||||
...(typeof title === 'object' ? title : {})
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
...combineGaugeConfig,
|
|
||||||
axisLine: {
|
|
||||||
...combineGaugeConfig.axisLine,
|
|
||||||
lineStyle: {
|
|
||||||
...combineGaugeConfig.axisLine.lineStyle,
|
|
||||||
color: [
|
|
||||||
[value / 100, colorValue],
|
|
||||||
[1, chartColorMap.gaugeBgColor]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: 'transparent'
|
|
||||||
},
|
|
||||||
detail: {
|
|
||||||
...combineGaugeConfig.detail,
|
|
||||||
borderColor: colorValue,
|
|
||||||
lineHeight: 20,
|
|
||||||
height: 18,
|
|
||||||
width: 50,
|
|
||||||
formatter: labelFormatter || gaugeItemConfig.detail.formatter
|
|
||||||
},
|
|
||||||
data: [{ value }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataOptions: any = setDataOptions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
height={height}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GaugeChart;
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|
||||||
const {
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
labelFormatter,
|
|
||||||
legendData,
|
|
||||||
maxItems,
|
|
||||||
title
|
|
||||||
} = props;
|
|
||||||
const {
|
|
||||||
token,
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
title: titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} = useChartConfig();
|
|
||||||
|
|
||||||
const dataOptions = useMemo((): any => {
|
|
||||||
const options = {
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
left: 'start'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...grid,
|
|
||||||
top: 0,
|
|
||||||
bottom: maxItems
|
|
||||||
? `${(1 / maxItems) * (maxItems - xAxisData.length) * 100}%`
|
|
||||||
: 0
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
...tooltip
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...xAxis,
|
|
||||||
axisLabel: {
|
|
||||||
...xAxis.axisLabel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...yAxis,
|
|
||||||
axisLabel: {
|
|
||||||
...yAxis.axisLabel,
|
|
||||||
show: true,
|
|
||||||
overflow: 'truncate',
|
|
||||||
width: 75,
|
|
||||||
ellipsis: '...',
|
|
||||||
margin: 8,
|
|
||||||
formatter(value: string, index: number) {
|
|
||||||
return `{a|${index + 1}}`;
|
|
||||||
},
|
|
||||||
rich: {
|
|
||||||
a: {
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 14,
|
|
||||||
color: token?.colorTextSecondary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
...legend,
|
|
||||||
data: []
|
|
||||||
},
|
|
||||||
|
|
||||||
series: []
|
|
||||||
};
|
|
||||||
const data = _.map(seriesData, (item: any) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: 20,
|
|
||||||
stack: 'Ad',
|
|
||||||
barGap: '20%',
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter(params: any) {
|
|
||||||
if (params.seriesIndex === 0) {
|
|
||||||
return `{value|${params.name}}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
position: 'left',
|
|
||||||
align: 'left',
|
|
||||||
offset: [5, 18],
|
|
||||||
rich: {
|
|
||||||
value: {
|
|
||||||
textBorderWidth: 0,
|
|
||||||
fontSize: 11,
|
|
||||||
color: token?.colorTextTertiary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: item.color
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
animation: false,
|
|
||||||
title: {
|
|
||||||
...options.title,
|
|
||||||
text: title
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...options.yAxis,
|
|
||||||
inverse: true,
|
|
||||||
type: 'category',
|
|
||||||
|
|
||||||
splitLine: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
data: xAxisData,
|
|
||||||
axisLine: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...options.xAxis,
|
|
||||||
type: 'value',
|
|
||||||
splitLine: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: data
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
title,
|
|
||||||
labelFormatter,
|
|
||||||
tooltip,
|
|
||||||
grid,
|
|
||||||
xAxis,
|
|
||||||
yAxis,
|
|
||||||
legend
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isEmpty = useMemo(() => {
|
|
||||||
return seriesData?.every?.((item: any) => {
|
|
||||||
return !item?.data?.length;
|
|
||||||
});
|
|
||||||
}, [seriesData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isEmpty ? (
|
|
||||||
<EmptyData
|
|
||||||
height={height}
|
|
||||||
title={_.get(title, 'text', title || '')}
|
|
||||||
></EmptyData>
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
height={height}
|
|
||||||
chartHeight={typeof height === 'number' ? height - 10 : undefined}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarChart;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import type {
|
|
||||||
BarSeriesOption,
|
|
||||||
GaugeSeriesOption,
|
|
||||||
LineSeriesOption,
|
|
||||||
ScatterSeriesOption
|
|
||||||
} from 'echarts/charts';
|
|
||||||
import { BarChart, GaugeChart, LineChart, ScatterChart } from 'echarts/charts';
|
|
||||||
import type {
|
|
||||||
DatasetComponentOption,
|
|
||||||
GridComponentOption,
|
|
||||||
TitleComponentOption,
|
|
||||||
TooltipComponentOption
|
|
||||||
} from 'echarts/components';
|
|
||||||
import {
|
|
||||||
DataZoomComponent,
|
|
||||||
DatasetComponent,
|
|
||||||
GridComponent,
|
|
||||||
LegendComponent,
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
// (filter, sort)
|
|
||||||
TransformComponent
|
|
||||||
} from 'echarts/components';
|
|
||||||
import type { ComposeOption } from 'echarts/core';
|
|
||||||
import * as echarts from 'echarts/core';
|
|
||||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
|
||||||
|
|
||||||
type ECOption = ComposeOption<
|
|
||||||
| BarSeriesOption
|
|
||||||
| LineSeriesOption
|
|
||||||
| TitleComponentOption
|
|
||||||
| TooltipComponentOption
|
|
||||||
| GridComponentOption
|
|
||||||
| DatasetComponentOption
|
|
||||||
| GaugeSeriesOption
|
|
||||||
| ScatterSeriesOption
|
|
||||||
>;
|
|
||||||
|
|
||||||
// register components and charts
|
|
||||||
echarts.use([
|
|
||||||
LegendComponent,
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
GridComponent,
|
|
||||||
DatasetComponent,
|
|
||||||
TransformComponent,
|
|
||||||
DataZoomComponent,
|
|
||||||
BarChart,
|
|
||||||
LineChart,
|
|
||||||
ScatterChart,
|
|
||||||
GaugeChart,
|
|
||||||
LabelLayout,
|
|
||||||
UniversalTransition,
|
|
||||||
CanvasRenderer
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type { ECOption };
|
|
||||||
|
|
||||||
export default echarts;
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import { genColors } from '@/utils';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import echarts from '.';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const LinearGradient = echarts.graphic.LinearGradient;
|
|
||||||
const LineChart: React.FC<ChartProps> = (props) => {
|
|
||||||
const {
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
yAxisName,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
labelFormatter,
|
|
||||||
tooltipValueFormatter = null,
|
|
||||||
legendData = [],
|
|
||||||
smooth,
|
|
||||||
title,
|
|
||||||
legendOptions,
|
|
||||||
gridOptions,
|
|
||||||
titleOptions,
|
|
||||||
showArea
|
|
||||||
} = props;
|
|
||||||
const {
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
lineItemConfig,
|
|
||||||
title: titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} = useChartConfig();
|
|
||||||
|
|
||||||
const axisLabelFormatter = (value: string, index: number) => {
|
|
||||||
if (labelFormatter) {
|
|
||||||
return labelFormatter(value, index);
|
|
||||||
}
|
|
||||||
if (index === xAxisData.length - 1) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
title: {
|
|
||||||
text: ''
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...grid,
|
|
||||||
...gridOptions
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
...tooltip,
|
|
||||||
formatter(params: any) {
|
|
||||||
return tooltipValueFormatter
|
|
||||||
? tooltip.formatter(params, tooltipValueFormatter)
|
|
||||||
: tooltip.formatter(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...xAxis,
|
|
||||||
axisLabel: {
|
|
||||||
...xAxis.axisLabel,
|
|
||||||
formatter: axisLabelFormatter
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis,
|
|
||||||
legend: {
|
|
||||||
...legend,
|
|
||||||
...legendOptions,
|
|
||||||
data: legendData.map((item: any) => {
|
|
||||||
return {
|
|
||||||
name: item,
|
|
||||||
icon: 'circle'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
series: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataOptions = useMemo((): any => {
|
|
||||||
const data = _.map(seriesData, (item: any) => {
|
|
||||||
const colors = genColors({
|
|
||||||
color: item.color,
|
|
||||||
alpha1: 0.25,
|
|
||||||
alpha2: 0.1
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...lineItemConfig,
|
|
||||||
smooth: smooth,
|
|
||||||
itemStyle: {
|
|
||||||
...lineItemConfig.itemStyle,
|
|
||||||
color: item.color
|
|
||||||
},
|
|
||||||
lineStyle: {
|
|
||||||
...lineItemConfig.lineStyle,
|
|
||||||
color: item.color
|
|
||||||
},
|
|
||||||
areaStyle: showArea
|
|
||||||
? {
|
|
||||||
color: new LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: colors[0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: colors[1]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
animation: false,
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
...titleOptions,
|
|
||||||
text: title
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...options.yAxis,
|
|
||||||
name: yAxisName,
|
|
||||||
nameTextStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
align: 'right'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...options.xAxis,
|
|
||||||
data: xAxisData
|
|
||||||
},
|
|
||||||
series: data
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
yAxisName,
|
|
||||||
title,
|
|
||||||
smooth,
|
|
||||||
titleOptions,
|
|
||||||
legendData,
|
|
||||||
options
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!seriesData.length ? (
|
|
||||||
<EmptyData
|
|
||||||
height={height}
|
|
||||||
title={_.get(title, 'text', title || '')}
|
|
||||||
></EmptyData>
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
height={height}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LineChart;
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import { genColors } from '@/utils';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import echarts from '.';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const LinearGradient = echarts.graphic.LinearGradient;
|
|
||||||
|
|
||||||
const MixLineBarChart: React.FC<
|
|
||||||
ChartProps & {
|
|
||||||
chartData: {
|
|
||||||
line: any[];
|
|
||||||
bar: any[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
> = (props) => {
|
|
||||||
const {
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
yAxisName,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
labelFormatter,
|
|
||||||
tooltipValueFormatter = null,
|
|
||||||
legendData = [],
|
|
||||||
smooth,
|
|
||||||
title,
|
|
||||||
chartData
|
|
||||||
} = props;
|
|
||||||
const {
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
lineItemConfig,
|
|
||||||
barItemConfig,
|
|
||||||
title: titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} = useChartConfig();
|
|
||||||
|
|
||||||
const { line: lineSeriesData, bar: barSeriesData } = chartData;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
title: {
|
|
||||||
text: ''
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...grid,
|
|
||||||
right: 0,
|
|
||||||
top: 20,
|
|
||||||
bottom: 10
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
...tooltip,
|
|
||||||
formatter(params: any) {
|
|
||||||
return tooltipValueFormatter
|
|
||||||
? tooltip.formatter(params, tooltipValueFormatter)
|
|
||||||
: tooltip.formatter(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
...xAxis,
|
|
||||||
axisLabel: {
|
|
||||||
...xAxis.axisLabel,
|
|
||||||
formatter: labelFormatter
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis,
|
|
||||||
legend: {
|
|
||||||
...legend,
|
|
||||||
data: legendData,
|
|
||||||
itemGap: 20,
|
|
||||||
bottom: 5,
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
|
|
||||||
series: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataOptions = useMemo((): any => {
|
|
||||||
const linedata = _.map(lineSeriesData, (item: any) => {
|
|
||||||
const colors = genColors({
|
|
||||||
color: item.color,
|
|
||||||
alpha1: 0.5,
|
|
||||||
alpha2: 0.1
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...lineItemConfig,
|
|
||||||
smooth: smooth,
|
|
||||||
itemStyle: {
|
|
||||||
...lineItemConfig.itemStyle,
|
|
||||||
color: item.color
|
|
||||||
},
|
|
||||||
yAxisIndex: 1,
|
|
||||||
lineStyle: {
|
|
||||||
...lineItemConfig.lineStyle,
|
|
||||||
color: item.color
|
|
||||||
}
|
|
||||||
// areaStyle: {
|
|
||||||
// color: new LinearGradient(0, 0, 0, 1, [
|
|
||||||
// {
|
|
||||||
// offset: 0,
|
|
||||||
// color: colors[0]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// offset: 1,
|
|
||||||
// color: colors[1]
|
|
||||||
// }
|
|
||||||
// ])
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const barData = _.map(barSeriesData, (item: any) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...barItemConfig,
|
|
||||||
stack: 'total',
|
|
||||||
yAxisIndex: 0,
|
|
||||||
itemStyle: {
|
|
||||||
...item.itemStyle,
|
|
||||||
color: item.color
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
animation: false,
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
text: title
|
|
||||||
},
|
|
||||||
yAxis: [
|
|
||||||
{
|
|
||||||
...options.yAxis
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options.yAxis,
|
|
||||||
nameTextStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
align: 'right'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
xAxis: {
|
|
||||||
...options.xAxis,
|
|
||||||
data: xAxisData
|
|
||||||
},
|
|
||||||
series: [...barData, ...linedata]
|
|
||||||
};
|
|
||||||
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!lineSeriesData.length && !barSeriesData.length ? (
|
|
||||||
<EmptyData
|
|
||||||
height={height}
|
|
||||||
title={_.get(title, 'text', title || '')}
|
|
||||||
></EmptyData>
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
height={height}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MixLineBarChart;
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
|
||||||
import useChartConfig from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { ChartProps } from './types';
|
|
||||||
|
|
||||||
const Scatter: React.FC<
|
|
||||||
ChartProps & {
|
|
||||||
xMax?: number;
|
|
||||||
yMax?: number;
|
|
||||||
}
|
|
||||||
> = (props) => {
|
|
||||||
const { grid, title: titleConfig, isDark, chartColorMap } = useChartConfig();
|
|
||||||
const {
|
|
||||||
seriesData,
|
|
||||||
xAxisData,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
showEmpty,
|
|
||||||
title,
|
|
||||||
xMax = 1,
|
|
||||||
yMax = 1
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const chart = useRef<any>(null);
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const colorMap = isDark
|
|
||||||
? {
|
|
||||||
split: chartColorMap.splitLineColor,
|
|
||||||
axis: chartColorMap.axislabelColor,
|
|
||||||
label: chartColorMap.axislabelColor
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
split: '#F2F2F2',
|
|
||||||
axis: '#dcdcdc',
|
|
||||||
label: '#dcdcdc'
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
animation: false,
|
|
||||||
grid: {
|
|
||||||
...grid,
|
|
||||||
right: 10,
|
|
||||||
top: 10,
|
|
||||||
bottom: 2,
|
|
||||||
left: 2,
|
|
||||||
containLabel: true,
|
|
||||||
borderRadius: 4
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
min: -xMax,
|
|
||||||
max: xMax,
|
|
||||||
scale: false,
|
|
||||||
slient: true,
|
|
||||||
splitNumber: 15,
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: colorMap.split
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: colorMap.axis
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
show: true,
|
|
||||||
color: colorMap.label
|
|
||||||
},
|
|
||||||
boundaryGap: [0.05, 0.05]
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
min: -yMax,
|
|
||||||
max: yMax,
|
|
||||||
scale: false,
|
|
||||||
slient: true,
|
|
||||||
splitNumber: 10,
|
|
||||||
boundaryGap: [0.05, 0.05],
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: colorMap.split
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: colorMap.axis
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
show: true,
|
|
||||||
color: colorMap.label
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
symbol: 'roundRect',
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
shadowColor: 'none',
|
|
||||||
textBorderColor: 'none',
|
|
||||||
formatter: (params: any) => {
|
|
||||||
return params.name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: []
|
|
||||||
};
|
|
||||||
}, [isDark, xMax, yMax]);
|
|
||||||
|
|
||||||
const findOverlappingPoints = useCallback(
|
|
||||||
(data: any[], currentPoint: any) => {
|
|
||||||
const overlappingPoints = [];
|
|
||||||
const symbolRadius = 16;
|
|
||||||
|
|
||||||
const [x1, y1] = chart.current.chart?.convertToPixel(
|
|
||||||
'grid',
|
|
||||||
currentPoint.value
|
|
||||||
);
|
|
||||||
|
|
||||||
const pixelPoints = data.map((point) => {
|
|
||||||
return {
|
|
||||||
...point,
|
|
||||||
value: chart.current.chart?.convertToPixel('grid', point.value)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let j = 0; j < pixelPoints.length; j++) {
|
|
||||||
if (currentPoint.name === pixelPoints[j].name) {
|
|
||||||
overlappingPoints.push({ ...pixelPoints[j] });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const [x2, y2] = pixelPoints[j].value;
|
|
||||||
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(_.round(x2 - x1, 2), 2) + Math.pow(_.round(y2 - y1, 2), 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distance <= symbolRadius) {
|
|
||||||
overlappingPoints.push({ ...pixelPoints[j] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return overlappingPoints;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderNameInTooltip = useCallback((dataList: any[]) => {
|
|
||||||
if (!dataList.length || dataList.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const renderText = (item: any) => {
|
|
||||||
return `<span class="tooltip-item-name">
|
|
||||||
<span style="display:flex;justify-content:center;align-items: center;color:#fff;
|
|
||||||
margin-right:0;border-radius:4px;width:14px;
|
|
||||||
height:14px;background-color:${item?.itemStyle?.color};"
|
|
||||||
>${item.name}</span>
|
|
||||||
</span>`;
|
|
||||||
};
|
|
||||||
return renderText;
|
|
||||||
}, []);
|
|
||||||
const dataOptions = useMemo((): any => {
|
|
||||||
const seriseDataList = seriesData.map((item: any, index: number) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#5470c6'
|
|
||||||
},
|
|
||||||
symbolSize: 16
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
borderWidth: 0,
|
|
||||||
backgroundColor: chartColorMap.colorBgContainerHover,
|
|
||||||
borderColor: 'transparent',
|
|
||||||
formatter(params: any, callback?: (val: any) => any) {
|
|
||||||
const dataList = findOverlappingPoints(seriseDataList, params.data);
|
|
||||||
let result = '';
|
|
||||||
const renderText: any = renderNameInTooltip(dataList);
|
|
||||||
dataList.forEach((item: any) => {
|
|
||||||
result += `
|
|
||||||
<span class="tooltip-item" style="justify-content: flex-start;">
|
|
||||||
${renderText ? renderText(item) : ''}
|
|
||||||
<span class="tooltip-value">${item.text}</span>
|
|
||||||
</span>`;
|
|
||||||
});
|
|
||||||
return `<div class="tooltip-wrapper scatter">${result}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
...titleConfig,
|
|
||||||
text: title
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
type: 'scatter',
|
|
||||||
labelLayout: {
|
|
||||||
hideOverlap: true
|
|
||||||
},
|
|
||||||
data: seriseDataList
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [seriesData, xAxisData, title, options, findOverlappingPoints]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!seriesData.length && showEmpty ? (
|
|
||||||
<EmptyData
|
|
||||||
height={height}
|
|
||||||
title={_.get(title, 'text', title || '')}
|
|
||||||
></EmptyData>
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
ref={chart}
|
|
||||||
height={height}
|
|
||||||
options={dataOptions}
|
|
||||||
width={width || '100%'}
|
|
||||||
></Chart>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Scatter;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type {
|
|
||||||
LegendComponentOption,
|
|
||||||
TitleComponentOption
|
|
||||||
} from 'echarts/components';
|
|
||||||
export interface ChartProps {
|
|
||||||
seriesData: any[];
|
|
||||||
showEmpty?: boolean;
|
|
||||||
showArea?: boolean;
|
|
||||||
xAxisData: string[];
|
|
||||||
legendData?: LegendComponentOption['data'];
|
|
||||||
legendOptions?: {
|
|
||||||
[K in keyof LegendComponentOption]?: LegendComponentOption[K];
|
|
||||||
};
|
|
||||||
gridOptions?: {
|
|
||||||
left?: string | number;
|
|
||||||
right?: string | number;
|
|
||||||
top?: string | number;
|
|
||||||
bottom?: string | number;
|
|
||||||
};
|
|
||||||
labelFormatter?: (val?: any, index?: number) => string;
|
|
||||||
tooltipValueFormatter?: (val: any) => string;
|
|
||||||
height: string | number;
|
|
||||||
width?: string | number;
|
|
||||||
title?: string | TitleComponentOption;
|
|
||||||
titleOptions?: {
|
|
||||||
[K in keyof TitleComponentOption]?: TitleComponentOption[K];
|
|
||||||
};
|
|
||||||
value?: number;
|
|
||||||
smooth?: boolean;
|
|
||||||
color?: string;
|
|
||||||
yAxisName?: string;
|
|
||||||
gaugeConfig?: {
|
|
||||||
radius?: string;
|
|
||||||
center?: string[];
|
|
||||||
startAngle?: number;
|
|
||||||
endAngle?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AreaChartItemProps {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
areaStyle: any;
|
|
||||||
data: { time: string; value: number }[];
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
.editor-wrap {
|
|
||||||
border-radius: var(--border-radius-mini);
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 0;
|
|
||||||
|
|
||||||
.code-pre {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-header {
|
|
||||||
display: flex;
|
|
||||||
padding-block: 0;
|
|
||||||
padding-inline: 12px 10px;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--color-editor-header-bg);
|
|
||||||
}
|
|
||||||
// set scrollbar style
|
|
||||||
.scrollbar {
|
|
||||||
.slider {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
const HeaderWrapper = styled.div<{ $height?: number }>`
|
|
||||||
display: flex;
|
|
||||||
padding-block: 0;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.bordered {
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
}
|
|
||||||
&.borderless {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.code-pre {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.scrollbar {
|
|
||||||
.slider {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface EditorwrapProps {
|
|
||||||
headerHeight?: number;
|
|
||||||
header?: React.ReactNode;
|
|
||||||
children: React.ReactNode;
|
|
||||||
variant?: 'bordered' | 'borderless';
|
|
||||||
styles?: {
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
header?: React.CSSProperties;
|
|
||||||
content?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const EditorWrap: React.FC<EditorwrapProps> = ({
|
|
||||||
headerHeight = 40,
|
|
||||||
header,
|
|
||||||
children,
|
|
||||||
variant = 'borderless',
|
|
||||||
styles = {}
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
style={{ ...styles.wrapper }}
|
|
||||||
className={classNames({
|
|
||||||
bordered: variant === 'bordered',
|
|
||||||
borderless: variant === 'borderless'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{header && (
|
|
||||||
<HeaderWrapper
|
|
||||||
style={{
|
|
||||||
height: headerHeight || 'auto'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{header}
|
|
||||||
</HeaderWrapper>
|
|
||||||
)}
|
|
||||||
<div>{children}</div>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditorWrap;
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
|
||||||
import Editor from '@monaco-editor/react';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef
|
|
||||||
} from 'react';
|
|
||||||
import EditorWrap from '../editor-wrap';
|
|
||||||
interface ViewerProps {
|
|
||||||
ref?: any;
|
|
||||||
lang: string;
|
|
||||||
defaultLang?: string;
|
|
||||||
config?: any;
|
|
||||||
value: string;
|
|
||||||
height?: string | number;
|
|
||||||
theme?: string;
|
|
||||||
header?: React.ReactNode;
|
|
||||||
placeholder?: string;
|
|
||||||
variant?: 'bordered' | 'borderless';
|
|
||||||
}
|
|
||||||
|
|
||||||
const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
|
||||||
const {
|
|
||||||
lang,
|
|
||||||
value,
|
|
||||||
config,
|
|
||||||
defaultLang,
|
|
||||||
height = 380,
|
|
||||||
theme = 'vs-dark',
|
|
||||||
header,
|
|
||||||
variant = 'borderless',
|
|
||||||
placeholder
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const handleBeforeMount = (monaco: any) => {
|
|
||||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
|
||||||
noSemanticValidation: false,
|
|
||||||
noSyntaxValidation: false,
|
|
||||||
diagnosticCodesToIgnore: [80001]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCode = () => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
setTimeout(() => {
|
|
||||||
editorRef.current
|
|
||||||
?.getAction?.('editor.action.formatDocument')
|
|
||||||
?.run()
|
|
||||||
.then(() => {
|
|
||||||
console.log('format success');
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
format: () => {
|
|
||||||
formatCode();
|
|
||||||
},
|
|
||||||
getValue: () => {
|
|
||||||
return editorRef.current?.getValue?.();
|
|
||||||
},
|
|
||||||
setValue: (val: string) => {
|
|
||||||
editorRef.current?.setValue?.(val);
|
|
||||||
},
|
|
||||||
editor: editorRef.current
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formatCode();
|
|
||||||
setTimeout(() => {
|
|
||||||
const lineCount = editorRef.current?.getModel().getLineCount();
|
|
||||||
editorRef.current?.revealLine(lineCount);
|
|
||||||
}, 100);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorWrap header={header} variant={variant}>
|
|
||||||
<Editor
|
|
||||||
height={height}
|
|
||||||
theme={theme}
|
|
||||||
className="monaco-editor"
|
|
||||||
defaultLanguage={defaultLang}
|
|
||||||
language={lang}
|
|
||||||
value={value}
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
scrollbar: {
|
|
||||||
verticalScrollbarSize: 6,
|
|
||||||
horizontalScrollbarSize: 6
|
|
||||||
},
|
|
||||||
placeholder: placeholder
|
|
||||||
}}
|
|
||||||
loading={<LoadingOutlined style={{ fontSize: 24 }}></LoadingOutlined>}
|
|
||||||
beforeMount={handleBeforeMount}
|
|
||||||
onMount={handleEditorDidMount}
|
|
||||||
/>
|
|
||||||
</EditorWrap>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ViewerEditor;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Empty } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const EmptyData: React.FC<{
|
|
||||||
height?: string | number;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
}> = ({ height, title }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: height || '100%'
|
|
||||||
}}
|
|
||||||
className="flex-center flex-column "
|
|
||||||
>
|
|
||||||
{title && (
|
|
||||||
<h3
|
|
||||||
className="justify-center font-size-12"
|
|
||||||
style={{ padding: '4px 0', marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="flex-center justify-center flex-column"
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
>
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmptyData;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Space } from 'antd';
|
|
||||||
|
|
||||||
type FormButtonsProps = {
|
|
||||||
onOk?: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
cancelText?: string;
|
|
||||||
okText?: string;
|
|
||||||
showOk?: boolean;
|
|
||||||
showCancel?: boolean;
|
|
||||||
htmlType?: 'submit' | 'button';
|
|
||||||
};
|
|
||||||
const FormButtons: React.FC<FormButtonsProps> = ({
|
|
||||||
onOk,
|
|
||||||
onCancel,
|
|
||||||
cancelText,
|
|
||||||
okText,
|
|
||||||
showCancel = true,
|
|
||||||
showOk = true,
|
|
||||||
htmlType = 'button'
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
|
||||||
<Space size={40} style={{ marginTop: '80px' }}>
|
|
||||||
{showOk && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={onOk}
|
|
||||||
style={{ width: '120px' }}
|
|
||||||
htmlType={htmlType}
|
|
||||||
>
|
|
||||||
{okText || intl.formatMessage({ id: 'common.button.save' })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{showCancel && (
|
|
||||||
<Button onClick={onCancel} style={{ width: '98px' }}>
|
|
||||||
{cancelText || intl.formatMessage({ id: 'common.button.cancel' })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormButtons;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import CodeViewer from './code-viewer';
|
|
||||||
import './styles/dark.less';
|
|
||||||
|
|
||||||
interface CodeViewerProps {
|
|
||||||
code: string;
|
|
||||||
copyValue?: string;
|
|
||||||
lang: string;
|
|
||||||
autodetect?: boolean;
|
|
||||||
ignoreIllegals?: boolean;
|
|
||||||
copyable?: boolean;
|
|
||||||
height?: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
xScrollable?: boolean;
|
|
||||||
}
|
|
||||||
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
|
||||||
const {
|
|
||||||
code,
|
|
||||||
copyValue,
|
|
||||||
lang,
|
|
||||||
autodetect,
|
|
||||||
ignoreIllegals,
|
|
||||||
copyable,
|
|
||||||
height = 'auto',
|
|
||||||
xScrollable = false
|
|
||||||
} = props || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeViewer
|
|
||||||
style={props.style}
|
|
||||||
height={height}
|
|
||||||
code={code}
|
|
||||||
copyValue={copyValue}
|
|
||||||
lang={lang}
|
|
||||||
theme="dark"
|
|
||||||
autodetect={autodetect}
|
|
||||||
ignoreIllegals={ignoreIllegals}
|
|
||||||
copyable={copyable}
|
|
||||||
xScrollable={xScrollable}
|
|
||||||
></CodeViewer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DarkViewer;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import CodeViewer from './code-viewer';
|
|
||||||
import './styles/light.less';
|
|
||||||
|
|
||||||
interface CodeViewerProps {
|
|
||||||
code: string;
|
|
||||||
copyValue?: string;
|
|
||||||
lang: string;
|
|
||||||
autodetect?: boolean;
|
|
||||||
ignoreIllegals?: boolean;
|
|
||||||
copyable?: boolean;
|
|
||||||
height?: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
xScrollable?: boolean;
|
|
||||||
}
|
|
||||||
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
|
||||||
const {
|
|
||||||
code,
|
|
||||||
copyValue,
|
|
||||||
lang,
|
|
||||||
autodetect,
|
|
||||||
ignoreIllegals,
|
|
||||||
copyable,
|
|
||||||
style,
|
|
||||||
height = 'auto',
|
|
||||||
xScrollable = false
|
|
||||||
} = props || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeViewer
|
|
||||||
style={style}
|
|
||||||
height={height}
|
|
||||||
code={code}
|
|
||||||
copyValue={copyValue}
|
|
||||||
lang={lang}
|
|
||||||
theme="light"
|
|
||||||
autodetect={autodetect}
|
|
||||||
ignoreIllegals={ignoreIllegals}
|
|
||||||
copyable={copyable}
|
|
||||||
xScrollable={xScrollable}
|
|
||||||
></CodeViewer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LightViewer;
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import hljs from 'highlight.js';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CopyButton from '../copy-button';
|
|
||||||
import { escapeHtml } from './utils';
|
|
||||||
|
|
||||||
interface CodeViewerProps {
|
|
||||||
code: string;
|
|
||||||
copyValue?: string;
|
|
||||||
lang: string;
|
|
||||||
autodetect?: boolean;
|
|
||||||
ignoreIllegals?: boolean;
|
|
||||||
copyable?: boolean;
|
|
||||||
height?: string | number;
|
|
||||||
theme?: 'light' | 'dark';
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
xScrollable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CodeHeaderProps {
|
|
||||||
copyValue: string;
|
|
||||||
copyable: boolean;
|
|
||||||
lang: string;
|
|
||||||
theme: 'light' | 'dark';
|
|
||||||
}
|
|
||||||
|
|
||||||
const CodeHeaderWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
background-color: #fafafa;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
&.dark {
|
|
||||||
background-color: var(--color-editor-header-bg);
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
border-radius: var(--border-radius-mini);
|
|
||||||
&:hover {
|
|
||||||
.custome-scrollbar {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--color-scrollbar-thumb);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CodeHeader: React.FC<CodeHeaderProps> = ({
|
|
||||||
copyValue,
|
|
||||||
lang,
|
|
||||||
theme,
|
|
||||||
copyable
|
|
||||||
}) => {
|
|
||||||
if (!copyable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<CodeHeaderWrapper
|
|
||||||
className={classNames({
|
|
||||||
dark: theme === 'dark',
|
|
||||||
light: theme === 'light'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span>{lang}</span>
|
|
||||||
<CopyButton
|
|
||||||
text={copyValue}
|
|
||||||
size="small"
|
|
||||||
style={{ color: '#abb2bf' }}
|
|
||||||
></CopyButton>
|
|
||||||
</CodeHeaderWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|
||||||
const {
|
|
||||||
code = '',
|
|
||||||
copyValue,
|
|
||||||
lang,
|
|
||||||
autodetect = true,
|
|
||||||
ignoreIllegals = true,
|
|
||||||
copyable = true,
|
|
||||||
height = 'auto',
|
|
||||||
style,
|
|
||||||
xScrollable = false
|
|
||||||
} = props || {};
|
|
||||||
|
|
||||||
const highlightedCode = useMemo(() => {
|
|
||||||
const autodetectLang = autodetect && !lang;
|
|
||||||
const cannotDetectLanguage = !autodetectLang && !hljs.getLanguage(lang);
|
|
||||||
let className = '';
|
|
||||||
|
|
||||||
if (!cannotDetectLanguage) {
|
|
||||||
className = `hljs ${lang}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No idea what language to use, return raw code
|
|
||||||
if (cannotDetectLanguage) {
|
|
||||||
console.warn(`The language "${lang}" you specified could not be found.`);
|
|
||||||
return {
|
|
||||||
value: escapeHtml(code),
|
|
||||||
className: className
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autodetectLang) {
|
|
||||||
const result = hljs.highlightAuto(code);
|
|
||||||
return {
|
|
||||||
value: result.value,
|
|
||||||
className: className
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const result = hljs.highlight(code, {
|
|
||||||
language: lang,
|
|
||||||
ignoreIllegals: ignoreIllegals
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
value: result.value,
|
|
||||||
className: className
|
|
||||||
};
|
|
||||||
}, [code, lang, autodetect, ignoreIllegals]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<CodeHeader
|
|
||||||
copyValue={copyValue || code}
|
|
||||||
lang={lang}
|
|
||||||
copyable={copyable}
|
|
||||||
theme={props.theme || 'light'}
|
|
||||||
></CodeHeader>
|
|
||||||
<pre
|
|
||||||
className={classNames(
|
|
||||||
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
|
||||||
{
|
|
||||||
dark: props.theme === 'dark',
|
|
||||||
light: props.theme === 'light',
|
|
||||||
'x-scrollable': xScrollable
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
marginBottom: 0,
|
|
||||||
height: height,
|
|
||||||
...style
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<code
|
|
||||||
style={{
|
|
||||||
minHeight: height,
|
|
||||||
...(xScrollable ? { width: 'max-content' } : {})
|
|
||||||
}}
|
|
||||||
className={classNames(highlightedCode.className, {
|
|
||||||
dark: props.theme === 'dark',
|
|
||||||
light: props.theme === 'light'
|
|
||||||
})}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: highlightedCode.value
|
|
||||||
}}
|
|
||||||
></code>
|
|
||||||
</pre>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CodeViewer;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import useUserSettings from '@/hooks/use-user-settings';
|
|
||||||
import React from 'react';
|
|
||||||
import CodeViewerDark from './code-viewer-dark';
|
|
||||||
import CodeViewerLight from './code-viewer-light';
|
|
||||||
import './styles/index.less';
|
|
||||||
|
|
||||||
const HighlightCode: React.FC<{
|
|
||||||
code: string;
|
|
||||||
lang?: string;
|
|
||||||
copyable?: boolean;
|
|
||||||
theme?: 'light' | 'dark';
|
|
||||||
fixedTheme?: 'light' | 'dark';
|
|
||||||
xScrollable?: boolean;
|
|
||||||
height?: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
copyValue?: string;
|
|
||||||
}> = (props) => {
|
|
||||||
const {
|
|
||||||
style,
|
|
||||||
code,
|
|
||||||
copyValue,
|
|
||||||
lang = 'bash',
|
|
||||||
copyable = true,
|
|
||||||
theme,
|
|
||||||
height = 'auto',
|
|
||||||
xScrollable = false
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { userSettings } = useUserSettings();
|
|
||||||
|
|
||||||
const currentTheme = React.useMemo(() => {
|
|
||||||
const res = theme || userSettings.theme === 'realDark' ? 'dark' : 'light';
|
|
||||||
return res;
|
|
||||||
}, [theme, userSettings.theme]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="high-light-wrapper hj-wrapper">
|
|
||||||
{currentTheme === 'dark' ? (
|
|
||||||
<CodeViewerDark
|
|
||||||
lang={lang}
|
|
||||||
code={code}
|
|
||||||
copyValue={copyValue}
|
|
||||||
copyable={copyable}
|
|
||||||
height={height}
|
|
||||||
xScrollable={xScrollable}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CodeViewerLight
|
|
||||||
style={style}
|
|
||||||
lang={lang}
|
|
||||||
code={code}
|
|
||||||
copyValue={copyValue}
|
|
||||||
copyable={copyable}
|
|
||||||
height={height}
|
|
||||||
xScrollable={xScrollable}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HighlightCode;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// @ts-ingore
|
|
||||||
pre code.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code.hljs {
|
|
||||||
padding: 3px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Atom One Dark by Daniel Gamage
|
|
||||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
|
||||||
|
|
||||||
base: #282c34
|
|
||||||
mono-1: #abb2bf
|
|
||||||
mono-2: #818896
|
|
||||||
mono-3: #5c6370
|
|
||||||
hue-1: #56b6c2
|
|
||||||
hue-2: #61aeee
|
|
||||||
hue-3: #c678dd
|
|
||||||
hue-4: #98c379
|
|
||||||
hue-5: #e06c75
|
|
||||||
hue-5-2: #be5046
|
|
||||||
hue-6: #d19a66
|
|
||||||
hue-6-2: #e6c07b
|
|
||||||
|
|
||||||
*/
|
|
||||||
.code-pre.dark {
|
|
||||||
.hljs {
|
|
||||||
color: #abb2bf;
|
|
||||||
background: var(--color-editor-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #5c6370;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-doctag,
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-formula {
|
|
||||||
color: #c678dd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-subst {
|
|
||||||
color: #e06c75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal {
|
|
||||||
color: #56b6c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-attribute,
|
|
||||||
.hljs-meta .hljs-string {
|
|
||||||
color: #98c379;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo,
|
|
||||||
.hljs-number {
|
|
||||||
color: #d19a66;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-link,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-title {
|
|
||||||
color: #61aeee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-title.class_,
|
|
||||||
.hljs-class .hljs-title {
|
|
||||||
color: #e6c07b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
.high-light-wrapper {
|
|
||||||
text-align: left;
|
|
||||||
font-size: var(--font-size-code);
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
font-weight: var(--font-weight-normal);
|
|
||||||
padding-inline: 0;
|
|
||||||
padding-block: 1.2em;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
height: var(--scrollbar-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.light {
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--color-scrollbar-thumb);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--scrollbar-handle-light-bg);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-pre {
|
|
||||||
padding-inline: 12px 12px;
|
|
||||||
border-radius: 0 0 var(--border-radius-mini) var(--border-radius-mini);
|
|
||||||
position: relative;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
code {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.copyable {
|
|
||||||
padding-inline: 12px 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
background-color: var(--color-editor-dark);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--scrollbar-handle-light-bg);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.light {
|
|
||||||
background-color: rgb(250, 250, 250);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--color-scrollbar-thumb);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// @ts-ingore
|
|
||||||
pre code.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code.hljs {
|
|
||||||
padding: 3px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Atom One Light by Daniel Gamage
|
|
||||||
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
|
||||||
|
|
||||||
base: #fafafa
|
|
||||||
mono-1: #383a42
|
|
||||||
mono-2: #686b77
|
|
||||||
mono-3: #a0a1a7
|
|
||||||
hue-1: #0184bb
|
|
||||||
hue-2: #4078f2
|
|
||||||
hue-3: #a626a4
|
|
||||||
hue-4: #50a14f
|
|
||||||
hue-5: #e45649
|
|
||||||
hue-5-2: #c91243
|
|
||||||
hue-6: #986801
|
|
||||||
hue-6-2: #c18401
|
|
||||||
|
|
||||||
*/
|
|
||||||
.code-pre.light {
|
|
||||||
.hljs {
|
|
||||||
color: #383a42;
|
|
||||||
background: var(--color-editor-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #a0a1a7;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-doctag,
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-formula {
|
|
||||||
color: #a626a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-subst {
|
|
||||||
color: #e45649;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal {
|
|
||||||
color: #0184bb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-attribute,
|
|
||||||
.hljs-meta .hljs-string {
|
|
||||||
color: #50a14f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-type,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo,
|
|
||||||
.hljs-number {
|
|
||||||
color: #986801;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-link,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-title {
|
|
||||||
color: #4078f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-title.class_,
|
|
||||||
.hljs-class .hljs-title {
|
|
||||||
color: #c18401;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-emphasis {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export function escapeHtml(value: string): string {
|
|
||||||
return value
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
@@ -1,915 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: iconfont; /* Project id 4613488 */
|
|
||||||
src: url('iconfont.woff2?t=1770985961668') format('woff2'),
|
|
||||||
url('iconfont.woff?t=1770985961668') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1770985961668') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-family: iconfont !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-grafana::before {
|
|
||||||
content: "\e61e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-chart::before {
|
|
||||||
content: "\e6da";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-monitor-02::before {
|
|
||||||
content: "\e6d9";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-monitor::before {
|
|
||||||
content: "\e6d8";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-metrics::before {
|
|
||||||
content: "\e6d6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-export::before {
|
|
||||||
content: "\e6d7";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-license::before {
|
|
||||||
content: "\e6d5";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-community::before {
|
|
||||||
content: "\e6d3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-person::before {
|
|
||||||
content: "\e6d4";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-public::before {
|
|
||||||
content: "\e6d2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-charger::before {
|
|
||||||
content: "\e6d0";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-disabled::before {
|
|
||||||
content: "\e6d1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-source::before {
|
|
||||||
content: "\e6cf";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-Serviceprovider::before {
|
|
||||||
content: "\e630";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-database::before {
|
|
||||||
content: "\e6ce";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-filters::before {
|
|
||||||
content: "\e6cc";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-speed-filled::before {
|
|
||||||
content: "\e6cb";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shield::before {
|
|
||||||
content: "\e6c9";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shield-filled::before {
|
|
||||||
content: "\e6ca";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-openai::before {
|
|
||||||
content: "\e61d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-anthropic::before {
|
|
||||||
content: "\e74d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-doubao::before {
|
|
||||||
content: "\e618";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-qwen::before {
|
|
||||||
content: "\e8b2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-deepseek::before {
|
|
||||||
content: "\e61c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-extension-outline::before {
|
|
||||||
content: "\e6c7";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-extension-filled::before {
|
|
||||||
content: "\e6c8";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-thead::before {
|
|
||||||
content: "\e617";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video-filled02::before {
|
|
||||||
content: "\e6c5";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video-outline::before {
|
|
||||||
content: "\e6c6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video::before {
|
|
||||||
content: "\e6c3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video-filled::before {
|
|
||||||
content: "\e6c4";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-refresh::before {
|
|
||||||
content: "\e6c2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-settings-02::before {
|
|
||||||
content: "\e6bf";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-arrow_forward::before {
|
|
||||||
content: "\e6c0";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-logout::before {
|
|
||||||
content: "\e6c1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-amd-logo::before {
|
|
||||||
content: "\e6be";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cloud::before {
|
|
||||||
content: "\e6bc";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-server02::before {
|
|
||||||
content: "\e6bd";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-drag_handle::before {
|
|
||||||
content: "\e6b9";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-basic::before {
|
|
||||||
content: "\e6bb";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-settings::before {
|
|
||||||
content: "\e6b8";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-speed::before {
|
|
||||||
content: "\e6ba";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-permission::before {
|
|
||||||
content: "\e6b6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-captive_portal::before {
|
|
||||||
content: "\e6b7";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-lock_open_right::before {
|
|
||||||
content: "\e6b3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-lock_person::before {
|
|
||||||
content: "\e6b4";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-lock_open::before {
|
|
||||||
content: "\e6b5";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-question::before {
|
|
||||||
content: "\e6b2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-aws::before {
|
|
||||||
content: "\e616";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-aws1::before {
|
|
||||||
content: "\e62f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-manage_user::before {
|
|
||||||
content: "\e6b1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-private::before {
|
|
||||||
content: "\e6b0";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stop3::before {
|
|
||||||
content: "\e6af";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-version::before {
|
|
||||||
content: "\e6ae";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-edit-content::before {
|
|
||||||
content: "\e6ab";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-code_block::before {
|
|
||||||
content: "\e6ac";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-parameters::before {
|
|
||||||
content: "\e6ad";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-backend-filled::before {
|
|
||||||
content: "\e6a9";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-backend::before {
|
|
||||||
content: "\e6aa";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-down2::before {
|
|
||||||
content: "\e6a7";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-nvidia2::before {
|
|
||||||
content: "\e60d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-centos::before {
|
|
||||||
content: "\e6cd";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-redhat::before {
|
|
||||||
content: "\ec7b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-ubuntu::before {
|
|
||||||
content: "\edd3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-debian::before {
|
|
||||||
content: "\eb74";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-alma-linux::before {
|
|
||||||
content: "\e6a8";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-fedora::before {
|
|
||||||
content: "\e61a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rocky-linux::before {
|
|
||||||
content: "\e620";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-nvidia1::before {
|
|
||||||
content: "\e980";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-nvidia::before {
|
|
||||||
content: "\e60c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-amd::before {
|
|
||||||
content: "\e6a6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-huawei::before {
|
|
||||||
content: "\e615";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-metax::before {
|
|
||||||
content: "\e6a4";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-ascend::before {
|
|
||||||
content: "\e6a3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-huaweicloud::before {
|
|
||||||
content: "\e614";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-alicloud::before {
|
|
||||||
content: "\e784";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-tencentcloud::before {
|
|
||||||
content: "\e609";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cluster2-outline::before {
|
|
||||||
content: "\e6a1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cluster2-filled::before {
|
|
||||||
content: "\e6a2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cluster-filled::before {
|
|
||||||
content: "\e69f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cluster-outline::before {
|
|
||||||
content: "\e6a0";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-admin-user::before {
|
|
||||||
content: "\e69d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-user::before {
|
|
||||||
content: "\e69e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-detail-info::before {
|
|
||||||
content: "\e69b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-docker::before {
|
|
||||||
content: "\e69c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-digitalocean::before {
|
|
||||||
content: "\eb79";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rocket-launch1::before {
|
|
||||||
content: "\e699";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rocket-launch-fill::before {
|
|
||||||
content: "\e69a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-credential-filled::before {
|
|
||||||
content: "\e697";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-credential-outline::before {
|
|
||||||
content: "\e698";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-k8s-filled::before {
|
|
||||||
content: "\e63e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-k8s-outline::before {
|
|
||||||
content: "\e64b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-huggingface1::before {
|
|
||||||
content: "\e605";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-modelscope_light::before {
|
|
||||||
content: "\e696";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-catalog1::before {
|
|
||||||
content: "\e691";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-chat-filled::before {
|
|
||||||
content: "\e692";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-chat::before {
|
|
||||||
content: "\e693";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-files-filled::before {
|
|
||||||
content: "\e694";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-files::before {
|
|
||||||
content: "\e695";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-models-filled::before {
|
|
||||||
content: "\e682";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-image-filled::before {
|
|
||||||
content: "\e683";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-audio1::before {
|
|
||||||
content: "\e684";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-image1::before {
|
|
||||||
content: "\e685";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-audio-filled::before {
|
|
||||||
content: "\e686";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-reranker-filled::before {
|
|
||||||
content: "\e687";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-embedding-filled::before {
|
|
||||||
content: "\e68a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-models::before {
|
|
||||||
content: "\e68b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-reranker::before {
|
|
||||||
content: "\e68c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-embedding::before {
|
|
||||||
content: "\e68d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-gpu-filled::before {
|
|
||||||
content: "\e68e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-catalog-filled::before {
|
|
||||||
content: "\e68f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-gpu1::before {
|
|
||||||
content: "\e690";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-language::before {
|
|
||||||
content: "\e679";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-help::before {
|
|
||||||
content: "\e67a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-key-filled::before {
|
|
||||||
content: "\e67b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-key::before {
|
|
||||||
content: "\e67d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-resources::before {
|
|
||||||
content: "\e67e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-users::before {
|
|
||||||
content: "\e67f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-resources-filled::before {
|
|
||||||
content: "\e680";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-users-filled::before {
|
|
||||||
content: "\e681";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-model::before {
|
|
||||||
content: "\e676";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-model-filled::before {
|
|
||||||
content: "\e678";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-layers-filled::before {
|
|
||||||
content: "\e671";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-layers::before {
|
|
||||||
content: "\e673";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-experiment::before {
|
|
||||||
content: "\e674";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-experiment-filled::before {
|
|
||||||
content: "\e675";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-dashboard::before {
|
|
||||||
content: "\e66d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-dashboard-filled::before {
|
|
||||||
content: "\e66e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-expand-left::before {
|
|
||||||
content: "\ea48";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-expand-right::before {
|
|
||||||
content: "\ea49";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-users-fill::before {
|
|
||||||
content: "\e677";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-layers-fill::before {
|
|
||||||
content: "\e89d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-key-fill::before {
|
|
||||||
content: "\e80e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-server-fill::before {
|
|
||||||
content: "\e7a3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-model-fill::before {
|
|
||||||
content: "\e7b7";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-left_panel_close::before {
|
|
||||||
content: "\e668";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-left_panel_open::before {
|
|
||||||
content: "\e669";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-playcircle-fill::before {
|
|
||||||
content: "\e665";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stopcircle-fill::before {
|
|
||||||
content: "\e666";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-play-speed::before {
|
|
||||||
content: "\e856";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-more::before {
|
|
||||||
content: "\e62e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-play::before {
|
|
||||||
content: "\e9f9";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-pause::before {
|
|
||||||
content: "\e713";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-dark_theme::before {
|
|
||||||
content: "\e646";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-theme-auto-1::before {
|
|
||||||
content: "\e660";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-theme-auto::before {
|
|
||||||
content: "\e663";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-auto-theme-1::before {
|
|
||||||
content: "\e664";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-auto-theme::before {
|
|
||||||
content: "\e662";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cols_3::before {
|
|
||||||
content: "\e65c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cols_6::before {
|
|
||||||
content: "\e65d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cols_2::before {
|
|
||||||
content: "\e65e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cols_4::before {
|
|
||||||
content: "\e65f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-save1::before {
|
|
||||||
content: "\e65b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-uncollapse_all::before {
|
|
||||||
content: "\e657";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-collapse::before {
|
|
||||||
content: "\e656";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rocket-launch::before {
|
|
||||||
content: "\e689";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-user-filled::before {
|
|
||||||
content: "\e625";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-assistant::before {
|
|
||||||
content: "\e62d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-assistant-filled::before {
|
|
||||||
content: "\e847";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-save3::before {
|
|
||||||
content: "\e655";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-fankuifaqs::before {
|
|
||||||
content: "\e7bf";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-issues::before {
|
|
||||||
content: "\e816";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-neicun::before {
|
|
||||||
content: "\e688";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-collapse_all::before {
|
|
||||||
content: "\e66f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-down::before {
|
|
||||||
content: "\e654";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-fenxiang::before {
|
|
||||||
content: "\e604";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-mosaic-2::before {
|
|
||||||
content: "\e64f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stars::before {
|
|
||||||
content: "\e8a8";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-mosaic::before {
|
|
||||||
content: "\e636";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-outline-play::before {
|
|
||||||
content: "\e653";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-SelectionInverse::before {
|
|
||||||
content: "\eace";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-justice1::before {
|
|
||||||
content: "\e652";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-New_img::before {
|
|
||||||
content: "\e733";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-new_release_outlined::before {
|
|
||||||
content: "\e66c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-new-releases::before {
|
|
||||||
content: "\e60f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-catalog::before {
|
|
||||||
content: "\e62b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-save2::before {
|
|
||||||
content: "\e635";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-left-template::before {
|
|
||||||
content: "\e62a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-logs::before {
|
|
||||||
content: "\e6ec";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-gpu::before {
|
|
||||||
content: "\e71e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-filled-gpu::before {
|
|
||||||
content: "\e6de";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-outline-gpu::before {
|
|
||||||
content: "\e641";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-ts-tubiao_webserver::before {
|
|
||||||
content: "\e716";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-server::before {
|
|
||||||
content: "\e66a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-host::before {
|
|
||||||
content: "\e7c6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-playcircle::before {
|
|
||||||
content: "\e80f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-recreate::before {
|
|
||||||
content: "\e6a5";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-save1::before {
|
|
||||||
content: "\e647";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-save::before {
|
|
||||||
content: "\e67c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-upload_image::before {
|
|
||||||
content: "\e613";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sound-wave::before {
|
|
||||||
content: "\e619";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rank1::before {
|
|
||||||
content: "\e7cb";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-cube::before {
|
|
||||||
content: "\e769";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-speaker-slash::before {
|
|
||||||
content: "\ebb6";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-random::before {
|
|
||||||
content: "\e603";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-suijisenlin::before {
|
|
||||||
content: "\e60e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stop2::before {
|
|
||||||
content: "\e8db";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stop::before {
|
|
||||||
content: "\e60b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-image::before {
|
|
||||||
content: "\e62c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-SpeakerSlash::before {
|
|
||||||
content: "\e661";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-SpeakerHigh::before {
|
|
||||||
content: "\e670";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-user_voice::before {
|
|
||||||
content: "\e667";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-audio::before {
|
|
||||||
content: "\e985";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-hard-disk::before {
|
|
||||||
content: "\eb1d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-new::before {
|
|
||||||
content: "\e612";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-tu2::before {
|
|
||||||
content: "\e607";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-robot::before {
|
|
||||||
content: "\e634";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-robot1::before {
|
|
||||||
content: "\e602";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-aizhineng::before {
|
|
||||||
content: "\e672";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-copy::before {
|
|
||||||
content: "\e720";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-AIzhineng::before {
|
|
||||||
content: "\e608";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-clear::before {
|
|
||||||
content: "\e60a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-keyboard::before {
|
|
||||||
content: "\e61b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-networkerror::before {
|
|
||||||
content: "\e624";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-external-link::before {
|
|
||||||
content: "\e66b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-huggingface::before {
|
|
||||||
content: "\e7d1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-ollama::before {
|
|
||||||
content: "\e601";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-layout6-line::before {
|
|
||||||
content: "\e9ef";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-Layout5::before {
|
|
||||||
content: "\e610";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-English::before {
|
|
||||||
content: "\e8b3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-chinese::before {
|
|
||||||
content: "\e611";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-yingguo::before {
|
|
||||||
content: "\e606";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-code::before {
|
|
||||||
content: "\e84f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-stop1::before {
|
|
||||||
content: "\e783";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-command::before {
|
|
||||||
content: "\e600";
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,71 +0,0 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
|
||||||
import {
|
|
||||||
ApiOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
DockerOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
ExperimentOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
KubernetesOutlined,
|
|
||||||
ProfileOutlined,
|
|
||||||
RetweetOutlined,
|
|
||||||
StarOutlined,
|
|
||||||
ThunderboltOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
EditOutlined: React.createElement(EditOutlined),
|
|
||||||
ExperimentOutlined: React.createElement(ExperimentOutlined),
|
|
||||||
DeleteOutlined: React.createElement(DeleteOutlined),
|
|
||||||
ThunderboltOutlined: React.createElement(ThunderboltOutlined),
|
|
||||||
RetweetOutlined: React.createElement(RetweetOutlined),
|
|
||||||
DownloadOutlined: React.createElement(DownloadOutlined),
|
|
||||||
FileTextOutlined: React.createElement(FileTextOutlined),
|
|
||||||
ApiOutlined: React.createElement(ApiOutlined),
|
|
||||||
KubernetesOutlined: React.createElement(KubernetesOutlined),
|
|
||||||
ProfileOutlined: React.createElement(ProfileOutlined),
|
|
||||||
DockerOutlined: React.createElement(DockerOutlined),
|
|
||||||
Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
|
|
||||||
Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
|
|
||||||
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
|
|
||||||
HF: React.createElement(IconFont, { type: 'icon-huggingface' }),
|
|
||||||
Ollama: React.createElement(IconFont, { type: 'icon-ollama' }),
|
|
||||||
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
|
|
||||||
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
|
|
||||||
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }),
|
|
||||||
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }),
|
|
||||||
Docker: React.createElement(IconFont, { type: 'icon-docker' }),
|
|
||||||
DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }),
|
|
||||||
DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }),
|
|
||||||
HuaweiCloud: React.createElement(IconFont, { type: 'icon-huaweicloud' }),
|
|
||||||
AliCloud: React.createElement(IconFont, { type: 'icon-alicloud' }),
|
|
||||||
TencentCloud: React.createElement(IconFont, { type: 'icon-tencentcloud' }),
|
|
||||||
Nvidia: React.createElement(IconFont, { type: 'icon-nvidia' }),
|
|
||||||
Ascend: React.createElement(IconFont, { type: 'icon-ascend' }),
|
|
||||||
Catalog1: React.createElement(IconFont, { type: 'icon-catalog1' }),
|
|
||||||
AMD: React.createElement(IconFont, { type: 'icon-amd' }),
|
|
||||||
KubernetesFilled: React.createElement(IconFont, { type: 'icon-k8s-filled' }),
|
|
||||||
EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }),
|
|
||||||
Yaml: React.createElement(IconFont, { type: 'icon-code_block' }),
|
|
||||||
Version: React.createElement(IconFont, { type: 'icon-version' }),
|
|
||||||
Parameter: React.createElement(IconFont, { type: 'icon-parameters' }),
|
|
||||||
Private: React.createElement(IconFont, { type: 'icon-private' }),
|
|
||||||
AWS: React.createElement(IconFont, { type: 'icon-aws' }),
|
|
||||||
LockOpenRight: React.createElement(IconFont, {
|
|
||||||
type: 'icon-lock_open_right'
|
|
||||||
}),
|
|
||||||
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
|
|
||||||
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
|
|
||||||
Permission: React.createElement(IconFont, { type: 'icon-permission' }),
|
|
||||||
CaptivePortal: React.createElement(IconFont, { type: 'icon-captive_portal' }),
|
|
||||||
StarOutlined: React.createElement(StarOutlined),
|
|
||||||
Charger: React.createElement(IconFont, { type: 'icon-charger' }),
|
|
||||||
Disabled: React.createElement(IconFont, { type: 'icon-disabled' }),
|
|
||||||
CopyOutlined: React.createElement(CopyOutlined),
|
|
||||||
Metrics: React.createElement(IconFont, { type: 'icon-metrics' })
|
|
||||||
};
|
|
||||||
|
|
||||||
export default icons;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
|
||||||
// import './iconfont/iconfont.js';
|
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_mk9rqojjoqk.js'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default IconFont;
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
/**
|
|
||||||
* Creates a canvas element and its rendering context.
|
|
||||||
* @param {number} width - Canvas width.
|
|
||||||
* @param {number} height - Canvas height.
|
|
||||||
* @returns {{ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D }} - The created canvas and context.
|
|
||||||
*/
|
|
||||||
function createCanvas(
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
return { canvas, ctx: canvas.getContext('2d')! };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a pixel is white.
|
|
||||||
* @param {Uint8ClampedArray} pixels - The image pixel data.
|
|
||||||
* @param {number} x - The pixel's X coordinate.
|
|
||||||
* @param {number} y - The pixel's Y coordinate.
|
|
||||||
* @param {number} width - The image width.
|
|
||||||
* @returns {boolean} - Whether the pixel is white.
|
|
||||||
*/
|
|
||||||
function isWhite(
|
|
||||||
pixels: Uint8ClampedArray,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number
|
|
||||||
): boolean {
|
|
||||||
let index = (y * width + x) * 4;
|
|
||||||
return (
|
|
||||||
pixels[index] === 255 &&
|
|
||||||
pixels[index + 1] === 255 &&
|
|
||||||
pixels[index + 2] === 255
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a flood fill to find all connected white pixels.
|
|
||||||
* @param {Uint8ClampedArray} pixels - The image pixel data.
|
|
||||||
* @param {number} width - The image width.
|
|
||||||
* @param {number} height - The image height.
|
|
||||||
* @param {number} x - The starting X coordinate.
|
|
||||||
* @param {number} y - The starting Y coordinate.
|
|
||||||
* @param {boolean[][]} visited - A 2D array to track visited pixels.
|
|
||||||
* @param {Array<{ x: number, y: number }>} block - The list of coordinates forming a white block.
|
|
||||||
*/
|
|
||||||
function floodFill(
|
|
||||||
pixels: Uint8ClampedArray,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
visited: boolean[][],
|
|
||||||
block: Array<{ x: number; y: number }>
|
|
||||||
): void {
|
|
||||||
let stack: Array<[number, number]> = [[x, y]];
|
|
||||||
let directions: Array<[number, number]> = [
|
|
||||||
[1, 0],
|
|
||||||
[-1, 0],
|
|
||||||
[0, 1],
|
|
||||||
[0, -1], // 4-directional search (can be extended to 8-directional)
|
|
||||||
[1, 1],
|
|
||||||
[-1, -1],
|
|
||||||
[1, -1],
|
|
||||||
[-1, 1] // Diagonal directions
|
|
||||||
];
|
|
||||||
|
|
||||||
while (stack.length) {
|
|
||||||
let [cx, cy] = stack.pop()!;
|
|
||||||
if (
|
|
||||||
cx < 0 ||
|
|
||||||
cy < 0 ||
|
|
||||||
cx >= width ||
|
|
||||||
cy >= height ||
|
|
||||||
visited[cy][cx] ||
|
|
||||||
!isWhite(pixels, cx, cy, width)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
visited[cy][cx] = true;
|
|
||||||
block.push({ x: cx, y: cy });
|
|
||||||
|
|
||||||
directions.forEach(([dx, dy]) => stack.push([cx + dx, cy + dy]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts all white blocks from an image.
|
|
||||||
* @param {ImageData} imageData - The image pixel data.
|
|
||||||
* @returns {Array<Array<{ x: number, y: number }>>} - List of white blocks, each containing pixel coordinates.
|
|
||||||
*/
|
|
||||||
function getWhiteBlocks(
|
|
||||||
imageData: ImageData
|
|
||||||
): Array<Array<{ x: number; y: number }>> {
|
|
||||||
const { data, width, height } = imageData;
|
|
||||||
let visited: boolean[][] = Array.from({ length: height }, () =>
|
|
||||||
new Array(width).fill(false)
|
|
||||||
);
|
|
||||||
let whiteBlocks: Array<Array<{ x: number; y: number }>> = [];
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
if (isWhite(data, x, y, width) && !visited[y][x]) {
|
|
||||||
let block: Array<{ x: number; y: number }> = [];
|
|
||||||
floodFill(data, width, height, x, y, visited, block);
|
|
||||||
whiteBlocks.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return whiteBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an image from a file.
|
|
||||||
* @param {File} file - The image file.
|
|
||||||
* @returns {Promise<HTMLImageElement>} - The loaded image element.
|
|
||||||
*/
|
|
||||||
function loadImage(file: string): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = file;
|
|
||||||
img.onload = () => resolve(img);
|
|
||||||
img.onerror = () => reject(new Error('Image loading failed'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the image file and extracts white blocks.
|
|
||||||
* @param {File} file - The uploaded image file.
|
|
||||||
* @returns {Promise<Array<Array<{ x: number, y: number }>>>} - List of white blocks with pixel coordinates.
|
|
||||||
*/
|
|
||||||
async function processImage(
|
|
||||||
file: string
|
|
||||||
): Promise<Array<Array<{ x: number; y: number }>>> {
|
|
||||||
const img = await loadImage(file);
|
|
||||||
const { canvas, ctx } = createCanvas(img.width, img.height);
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
|
||||||
const whiteBlocks = getWhiteBlocks(imageData);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(img.src);
|
|
||||||
return whiteBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { processImage };
|
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { useCallback, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
type Point = { x: number; y: number; lineWidth: number };
|
|
||||||
type Stroke = Point[];
|
|
||||||
const COLOR = 'rgba(0, 0, 255, 0.3)';
|
|
||||||
|
|
||||||
export default function useDrawing(props: {
|
|
||||||
isDisabled?: boolean;
|
|
||||||
invertMask: boolean;
|
|
||||||
maskUpload?: any[];
|
|
||||||
lineWidth: number;
|
|
||||||
translatePos: { current: { x: number; y: number } };
|
|
||||||
onSave: (imageData: { mask: string | null; img: string }) => void;
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
isDisabled,
|
|
||||||
invertMask,
|
|
||||||
maskUpload,
|
|
||||||
lineWidth,
|
|
||||||
translatePos,
|
|
||||||
onSave
|
|
||||||
} = props;
|
|
||||||
const mouseDownState = useRef<boolean>(false);
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const offscreenCanvasRef = useRef<any>(null);
|
|
||||||
const currentStroke = useRef<Point[]>([]);
|
|
||||||
const strokesRef = useRef<Stroke[]>([]);
|
|
||||||
const isDrawing = useRef<boolean>(false);
|
|
||||||
const cursorRef = useRef<HTMLDivElement>(null);
|
|
||||||
const autoScale = useRef<number>(1);
|
|
||||||
const baseScale = useRef<number>(1);
|
|
||||||
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
||||||
const maskStorkeRef = useRef<Stroke[]>([]);
|
|
||||||
const isLoadingMaskRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
const disabled = useMemo(() => {
|
|
||||||
return isDisabled || invertMask || !!maskUpload?.length;
|
|
||||||
}, [isDisabled, invertMask, maskUpload]);
|
|
||||||
|
|
||||||
const setStrokes = (strokes: Stroke[]) => {
|
|
||||||
strokesRef.current = strokes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMaskStrokes = (strokes: Stroke[]) => {
|
|
||||||
maskStorkeRef.current = strokes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const inpaintArea = useCallback(
|
|
||||||
(data: Uint8ClampedArray<ArrayBufferLike>) => {
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
const alpha = data[i + 3];
|
|
||||||
if (alpha > 0) {
|
|
||||||
data[i] = 255; // Red
|
|
||||||
data[i + 1] = 255; // Green
|
|
||||||
data[i + 2] = 255; // Blue
|
|
||||||
data[i + 3] = 255; // Alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const generateImage = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
return canvas.toDataURL('image/png');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const generateMask = useCallback(() => {
|
|
||||||
if (strokesRef.current.length === 0 && maskStorkeRef.current.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
|
||||||
const maskCanvas = document.createElement('canvas');
|
|
||||||
maskCanvas.width = overlayCanvas.width;
|
|
||||||
maskCanvas.height = overlayCanvas.height;
|
|
||||||
const maskCtx = maskCanvas.getContext('2d')!;
|
|
||||||
const overlayCtx = overlayCanvas.getContext('2d')!;
|
|
||||||
|
|
||||||
const imageData = overlayCtx.getImageData(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
overlayCanvas.width,
|
|
||||||
overlayCanvas.height
|
|
||||||
);
|
|
||||||
const data = imageData.data;
|
|
||||||
|
|
||||||
inpaintArea(data);
|
|
||||||
|
|
||||||
maskCtx.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
maskCtx.globalCompositeOperation = 'destination-over';
|
|
||||||
maskCtx.fillStyle = 'black';
|
|
||||||
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
|
|
||||||
|
|
||||||
return maskCanvas.toDataURL('image/png');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const saveImage = () => {
|
|
||||||
const mask = generateMask();
|
|
||||||
const img = generateImage();
|
|
||||||
onSave({ mask, img });
|
|
||||||
};
|
|
||||||
|
|
||||||
const creatOffscreenCanvas = useCallback(() => {
|
|
||||||
if (!offscreenCanvasRef.current) {
|
|
||||||
offscreenCanvasRef.current = document.createElement('canvas');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTransform = useCallback(() => {
|
|
||||||
creatOffscreenCanvas();
|
|
||||||
const ctx = canvasRef.current?.getContext('2d');
|
|
||||||
const overlayCtx = overlayCanvasRef.current?.getContext('2d');
|
|
||||||
const offCtx = offscreenCanvasRef.current?.getContext('2d');
|
|
||||||
|
|
||||||
if (!ctx || !overlayCtx) return;
|
|
||||||
|
|
||||||
ctx!.resetTransform();
|
|
||||||
overlayCtx!.resetTransform();
|
|
||||||
offCtx!.resetTransform();
|
|
||||||
|
|
||||||
const { current: scale } = autoScale;
|
|
||||||
const { x: translateX, y: translateY } = translatePos.current;
|
|
||||||
ctx!.setTransform(scale, 0, 0, scale, translateX, translateY);
|
|
||||||
overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
|
|
||||||
offCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getTransformedPoint = (offsetX: number, offsetY: number) => {
|
|
||||||
const { current: scale } = autoScale;
|
|
||||||
console.log('lineWidth:----------', lineWidth, autoScale.current);
|
|
||||||
|
|
||||||
const { x: translateX, y: translateY } = translatePos.current;
|
|
||||||
|
|
||||||
const transformedX = (offsetX - translateX) / scale;
|
|
||||||
const transformedY = (offsetY - translateY) / scale;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: transformedX,
|
|
||||||
y: transformedY
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTransformLineWidth = (w = 1) => {
|
|
||||||
console.log('lineWidth:', lineWidth, autoScale.current);
|
|
||||||
const width = w || lineWidth;
|
|
||||||
return width / autoScale.current;
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawLine = useCallback(
|
|
||||||
(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
point: Point,
|
|
||||||
options: {
|
|
||||||
lineWidth: number;
|
|
||||||
color: string;
|
|
||||||
compositeOperation: 'source-over' | 'destination-out';
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { lineWidth, color, compositeOperation } = options;
|
|
||||||
|
|
||||||
ctx.lineWidth = getTransformLineWidth(lineWidth);
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.globalCompositeOperation = compositeOperation;
|
|
||||||
|
|
||||||
const { x, y } = getTransformedPoint(point.x, point.y);
|
|
||||||
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
if (compositeOperation === 'source-over') {
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
},
|
|
||||||
[getTransformLineWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
const drawStroke = useCallback(
|
|
||||||
(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
stroke: Stroke | Point[],
|
|
||||||
options: {
|
|
||||||
lineWidth?: number;
|
|
||||||
color: string;
|
|
||||||
compositeOperation: 'source-over' | 'destination-out';
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { color, compositeOperation } = options;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.globalCompositeOperation = compositeOperation;
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
|
|
||||||
stroke.forEach((point, i) => {
|
|
||||||
const { x, y } = getTransformedPoint(point.x, point.y);
|
|
||||||
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
|
|
||||||
if (i === 0) {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (compositeOperation === 'source-over') {
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
},
|
|
||||||
[getTransformLineWidth, getTransformedPoint]
|
|
||||||
);
|
|
||||||
|
|
||||||
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
'Drawing:',
|
|
||||||
isDrawing.current,
|
|
||||||
currentStroke.current,
|
|
||||||
strokesRef.current
|
|
||||||
);
|
|
||||||
if (!isDrawing.current || !mouseDownState.current) return;
|
|
||||||
|
|
||||||
const { offsetX, offsetY } = e.nativeEvent;
|
|
||||||
const currentX = offsetX;
|
|
||||||
const currentY = offsetY;
|
|
||||||
console.log('currentStroke:', currentStroke.current);
|
|
||||||
currentStroke.current.push({
|
|
||||||
x: currentX,
|
|
||||||
y: currentY,
|
|
||||||
lineWidth
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d');
|
|
||||||
|
|
||||||
ctx!.save();
|
|
||||||
|
|
||||||
drawLine(
|
|
||||||
ctx!,
|
|
||||||
{ x: currentX, y: currentY, lineWidth },
|
|
||||||
{ lineWidth, color: COLOR, compositeOperation: 'destination-out' }
|
|
||||||
);
|
|
||||||
drawLine(
|
|
||||||
ctx!,
|
|
||||||
{ x: currentX, y: currentY, lineWidth },
|
|
||||||
{ lineWidth, color: COLOR, compositeOperation: 'source-over' }
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx!.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDrawing.current = true;
|
|
||||||
|
|
||||||
currentStroke.current = [];
|
|
||||||
const { offsetX, offsetY } = e.nativeEvent;
|
|
||||||
|
|
||||||
const currentX = offsetX;
|
|
||||||
const currentY = offsetY;
|
|
||||||
|
|
||||||
currentStroke.current.push({
|
|
||||||
x: currentX,
|
|
||||||
y: currentY,
|
|
||||||
lineWidth
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d');
|
|
||||||
setTransform();
|
|
||||||
const { x, y } = getTransformedPoint(currentX, currentY);
|
|
||||||
ctx!.beginPath();
|
|
||||||
ctx!.moveTo(x, y);
|
|
||||||
|
|
||||||
draw(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const endDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isDrawing.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('End Drawing:', e);
|
|
||||||
|
|
||||||
isDrawing.current = false;
|
|
||||||
|
|
||||||
strokesRef.current.push(_.cloneDeep(currentStroke.current));
|
|
||||||
|
|
||||||
currentStroke.current = [];
|
|
||||||
|
|
||||||
saveImage();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
overlayCanvasRef.current!.style.cursor = 'none';
|
|
||||||
cursorRef.current!.style.display = 'block';
|
|
||||||
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isDrawing.current = false;
|
|
||||||
overlayCanvasRef.current!.style.cursor = 'default';
|
|
||||||
cursorRef.current!.style.display = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseEnter = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
console.log('mouse enter:', mouseDownState.current);
|
|
||||||
if (disabled) {
|
|
||||||
overlayCanvasRef.current!.style.cursor = 'default';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if (mouseDownState.current) {
|
|
||||||
// isDrawing.current = true;
|
|
||||||
// }
|
|
||||||
overlayCanvasRef.current!.style.cursor = 'none';
|
|
||||||
cursorRef.current!.style.display = 'block';
|
|
||||||
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearOverlayCanvas = useCallback(() => {
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d');
|
|
||||||
const offCtx = offscreenCanvasRef.current?.getContext('2d');
|
|
||||||
|
|
||||||
offCtx!.resetTransform();
|
|
||||||
ctx!.resetTransform();
|
|
||||||
ctx!.clearRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
overlayCanvasRef.current!.width,
|
|
||||||
overlayCanvasRef.current!.height
|
|
||||||
);
|
|
||||||
offCtx!.clearRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
overlayCanvasRef.current!.width,
|
|
||||||
overlayCanvasRef.current!.height
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearCanvas = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const ctx = canvasRef.current!.getContext('2d');
|
|
||||||
const offCtx = offscreenCanvasRef.current?.getContext('2d');
|
|
||||||
ctx!.resetTransform();
|
|
||||||
ctx!.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
offCtx!.resetTransform();
|
|
||||||
offCtx!.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resetCanvas = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
|
||||||
const offscreenCanvas = offscreenCanvasRef.current!;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const overlayCtx = overlayCanvas.getContext('2d');
|
|
||||||
const offCtx = offscreenCanvasRef.current?.getContext('2d');
|
|
||||||
|
|
||||||
autoScale.current = 1;
|
|
||||||
baseScale.current = 1;
|
|
||||||
translatePos.current = { x: 0, y: 0 };
|
|
||||||
contentPos.current = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
canvas.style.transform = 'scale(1)';
|
|
||||||
overlayCanvas.style.transform = 'scale(1)';
|
|
||||||
offscreenCanvas.style.transform = 'scale(1)';
|
|
||||||
|
|
||||||
cursorRef.current!.style.width = `${lineWidth}px`;
|
|
||||||
cursorRef.current!.style.height = `${lineWidth}px`;
|
|
||||||
|
|
||||||
ctx!.resetTransform();
|
|
||||||
overlayCtx!.resetTransform();
|
|
||||||
offCtx!.resetTransform();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fitView = () => {
|
|
||||||
resetCanvas();
|
|
||||||
autoScale.current = baseScale.current;
|
|
||||||
translatePos.current = { x: 0, y: 0 };
|
|
||||||
setTransform();
|
|
||||||
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
|
|
||||||
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
|
|
||||||
offscreenCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvasRef,
|
|
||||||
overlayCanvasRef,
|
|
||||||
offscreenCanvasRef,
|
|
||||||
cursorRef,
|
|
||||||
strokesRef,
|
|
||||||
currentStroke,
|
|
||||||
isDrawing,
|
|
||||||
mouseDownState,
|
|
||||||
autoScale,
|
|
||||||
baseScale,
|
|
||||||
maskStorkeRef,
|
|
||||||
isLoadingMaskRef,
|
|
||||||
creatOffscreenCanvas,
|
|
||||||
setMaskStrokes,
|
|
||||||
fitView,
|
|
||||||
setStrokes,
|
|
||||||
resetCanvas,
|
|
||||||
draw,
|
|
||||||
getTransformLineWidth,
|
|
||||||
getTransformedPoint,
|
|
||||||
drawStroke,
|
|
||||||
startDrawing,
|
|
||||||
endDrawing,
|
|
||||||
handleMouseMove,
|
|
||||||
handleMouseLeave,
|
|
||||||
handleMouseEnter,
|
|
||||||
saveImage,
|
|
||||||
generateMask,
|
|
||||||
generateImage,
|
|
||||||
setTransform,
|
|
||||||
clearOverlayCanvas,
|
|
||||||
clearCanvas
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { MutableRefObject, useState } from 'react';
|
|
||||||
|
|
||||||
export default function useZoom(props: {
|
|
||||||
overlayCanvasRef: any;
|
|
||||||
canvasRef: any;
|
|
||||||
offscreenCanvasRef: any;
|
|
||||||
cursorRef: any;
|
|
||||||
lineWidth: number;
|
|
||||||
autoScale: MutableRefObject<number>;
|
|
||||||
baseScale: MutableRefObject<number>;
|
|
||||||
translatePos: MutableRefObject<{ x: number; y: number }>;
|
|
||||||
isLoadingMaskRef: MutableRefObject<boolean>;
|
|
||||||
}) {
|
|
||||||
const MIN_SCALE = 0.2;
|
|
||||||
const MAX_SCALE = 8;
|
|
||||||
const ZOOM_SPEED = 0.1;
|
|
||||||
const {
|
|
||||||
overlayCanvasRef,
|
|
||||||
canvasRef,
|
|
||||||
offscreenCanvasRef,
|
|
||||||
cursorRef,
|
|
||||||
lineWidth,
|
|
||||||
translatePos,
|
|
||||||
autoScale,
|
|
||||||
baseScale,
|
|
||||||
isLoadingMaskRef
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [activeScale, setActiveScale] = useState<number>(1);
|
|
||||||
|
|
||||||
const setCanvasTransformOrigin = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (autoScale.current <= MIN_SCALE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoScale.current >= MAX_SCALE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = overlayCanvasRef.current!.getBoundingClientRect();
|
|
||||||
const mouseX = e.clientX - rect.left;
|
|
||||||
const mouseY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const originX = mouseX / rect.width;
|
|
||||||
const originY = mouseY / rect.height;
|
|
||||||
|
|
||||||
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
|
|
||||||
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
|
|
||||||
offscreenCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => {
|
|
||||||
const newScale = _.round(autoScale.current + scaleChange, 2);
|
|
||||||
|
|
||||||
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return;
|
|
||||||
|
|
||||||
const { current: oldScale } = autoScale;
|
|
||||||
const { x: oldTranslateX, y: oldTranslateY } = translatePos.current;
|
|
||||||
|
|
||||||
const centerX = (mouseX - oldTranslateX) / oldScale;
|
|
||||||
const centerY = (mouseY - oldTranslateY) / oldScale;
|
|
||||||
|
|
||||||
autoScale.current = newScale;
|
|
||||||
|
|
||||||
const newTranslateX = mouseX - centerX * newScale;
|
|
||||||
const newTranslateY = mouseY - centerY * newScale;
|
|
||||||
|
|
||||||
translatePos.current = { x: newTranslateX, y: newTranslateY };
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyCanvasTransform = () => {
|
|
||||||
const scale = autoScale.current;
|
|
||||||
const transform = `scale(${scale})`;
|
|
||||||
|
|
||||||
overlayCanvasRef.current!.style.transform = transform;
|
|
||||||
canvasRef.current!.style.transform = transform;
|
|
||||||
offscreenCanvasRef.current!.style.transform = transform;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleZoom = (event: React.WheelEvent<HTMLCanvasElement>) => {
|
|
||||||
const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED;
|
|
||||||
|
|
||||||
// current mouse position
|
|
||||||
const canvas = overlayCanvasRef.current!;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
const mouseX = event.clientX - rect.left;
|
|
||||||
const mouseY = event.clientY - rect.top;
|
|
||||||
applyCanvasTransform();
|
|
||||||
|
|
||||||
setCanvasTransformOrigin(event);
|
|
||||||
updateZoom(scaleChange, mouseX, mouseY);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCursorSize = () => {
|
|
||||||
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
|
|
||||||
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCursorPosOnZoom = (e: any) => {
|
|
||||||
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnWheel = (event: any) => {
|
|
||||||
if (isLoadingMaskRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// stop
|
|
||||||
handleZoom(event);
|
|
||||||
updateCursorSize();
|
|
||||||
updateCursorPosOnZoom(event);
|
|
||||||
setActiveScale(autoScale.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
const throttleHandleOnWheel = _.throttle((event: any) => {
|
|
||||||
handleOnWheel(event);
|
|
||||||
}, 16);
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleOnWheel: handleOnWheel,
|
|
||||||
setActiveScale,
|
|
||||||
activeScale,
|
|
||||||
autoScale,
|
|
||||||
baseScale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
.editor-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.tools {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-canvas:hover {
|
|
||||||
cursor: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-canvas.overlay-canvas--disabled:hover {
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-mask {
|
|
||||||
&:hover {
|
|
||||||
.close-btn {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,645 +0,0 @@
|
|||||||
import { Spin } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useDrawing from './hooks/use-drawing';
|
|
||||||
import useZoom from './hooks/use-zoom';
|
|
||||||
import './index.less';
|
|
||||||
import { ImageActionsBar, ToolsBar } from './tools-bar';
|
|
||||||
|
|
||||||
const LoadWrapper = styled.div<{ width?: number; height?: number }>`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: ${(props) => `${props.width}px` || '100%'};
|
|
||||||
z-index: 100;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Loading = (props: { width: number; height: number }) => {
|
|
||||||
const { width, height } = props;
|
|
||||||
return (
|
|
||||||
<LoadWrapper width={width} height={height}>
|
|
||||||
<Spin spinning size="middle"></Spin>
|
|
||||||
</LoadWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Point = { x: number; y: number; lineWidth: number };
|
|
||||||
type Stroke = Point[];
|
|
||||||
|
|
||||||
type CanvasImageEditorProps = {
|
|
||||||
ref?: any;
|
|
||||||
loading: boolean;
|
|
||||||
imageSrc: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
imguid: string | number;
|
|
||||||
maskUpload?: any[];
|
|
||||||
clearUploadMask?: () => void;
|
|
||||||
onSave: (imageData: { mask: string | null; img: string }) => void;
|
|
||||||
onScaleImageSize?: (data: { width: number; height: number }) => void;
|
|
||||||
handleUpdateImageList: (fileList: any[]) => void;
|
|
||||||
handleUpdateMaskList: (fileList: any[]) => void;
|
|
||||||
uploadButton?: React.ReactNode;
|
|
||||||
accept?: string;
|
|
||||||
imageStatus: {
|
|
||||||
isOriginal: boolean;
|
|
||||||
isResetNeeded: boolean;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOR = 'rgba(0, 0, 255, 0.3)';
|
|
||||||
|
|
||||||
const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
loading,
|
|
||||||
imageSrc,
|
|
||||||
disabled: isDisabled,
|
|
||||||
imageStatus,
|
|
||||||
maskUpload,
|
|
||||||
accept,
|
|
||||||
onSave,
|
|
||||||
onScaleImageSize,
|
|
||||||
handleUpdateImageList,
|
|
||||||
handleUpdateMaskList
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const invertWorkerRef = useRef<any>(null);
|
|
||||||
const loadMaksWorkerRef = useRef<any>(null);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [lineWidth, setLineWidth] = useState<number>(60);
|
|
||||||
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
||||||
const negativeMaskRef = useRef<boolean>(false);
|
|
||||||
const [invertMask, setInvertMask] = useState<boolean>(false);
|
|
||||||
const timer = useRef<any>(null);
|
|
||||||
const [loadingSize, setLoadingSize] = useState({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
loading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
canvasRef,
|
|
||||||
overlayCanvasRef,
|
|
||||||
offscreenCanvasRef,
|
|
||||||
cursorRef,
|
|
||||||
strokesRef,
|
|
||||||
currentStroke,
|
|
||||||
mouseDownState,
|
|
||||||
autoScale,
|
|
||||||
baseScale,
|
|
||||||
maskStorkeRef,
|
|
||||||
isLoadingMaskRef,
|
|
||||||
setMaskStrokes,
|
|
||||||
draw,
|
|
||||||
drawStroke,
|
|
||||||
startDrawing,
|
|
||||||
endDrawing,
|
|
||||||
handleMouseMove,
|
|
||||||
handleMouseLeave,
|
|
||||||
handleMouseEnter,
|
|
||||||
saveImage,
|
|
||||||
resetCanvas,
|
|
||||||
creatOffscreenCanvas,
|
|
||||||
generateMask,
|
|
||||||
getTransformLineWidth,
|
|
||||||
getTransformedPoint,
|
|
||||||
setStrokes,
|
|
||||||
setTransform,
|
|
||||||
clearOverlayCanvas,
|
|
||||||
clearCanvas,
|
|
||||||
fitView
|
|
||||||
} = useDrawing({
|
|
||||||
lineWidth: lineWidth,
|
|
||||||
translatePos: translatePos,
|
|
||||||
isDisabled: isDisabled,
|
|
||||||
invertMask,
|
|
||||||
maskUpload,
|
|
||||||
onSave: onSave
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleOnWheel, setActiveScale, activeScale } = useZoom({
|
|
||||||
overlayCanvasRef,
|
|
||||||
canvasRef,
|
|
||||||
offscreenCanvasRef,
|
|
||||||
cursorRef,
|
|
||||||
lineWidth,
|
|
||||||
translatePos,
|
|
||||||
autoScale,
|
|
||||||
baseScale,
|
|
||||||
isLoadingMaskRef
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabled = useMemo(() => {
|
|
||||||
return isDisabled || invertMask || !!maskUpload?.length;
|
|
||||||
}, [isDisabled, invertMask, maskUpload]);
|
|
||||||
|
|
||||||
// update the canvas size
|
|
||||||
const updateCanvasSize = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
|
||||||
const offscreenCanvas = offscreenCanvasRef.current!;
|
|
||||||
|
|
||||||
overlayCanvas.width = canvas.width;
|
|
||||||
overlayCanvas.height = canvas.height;
|
|
||||||
offscreenCanvas.width = canvas.width;
|
|
||||||
offscreenCanvas.height = canvas.height;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const downloadMask = useCallback(() => {
|
|
||||||
const mask = generateMask();
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = `mask_${dayjs().format('YYYYMMDDHHmmss')}.png`;
|
|
||||||
link.href = mask || '';
|
|
||||||
link.click();
|
|
||||||
}, [generateMask]);
|
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
|
||||||
clearOverlayCanvas();
|
|
||||||
setStrokes([]);
|
|
||||||
setMaskStrokes([]);
|
|
||||||
saveImage();
|
|
||||||
currentStroke.current = [];
|
|
||||||
console.log('Resetting strokes', currentStroke.current);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadMaskPixs = (maskStrokes: Stroke[], mainStrokes?: Stroke[]) => {
|
|
||||||
try {
|
|
||||||
if (!maskStrokes.length && !mainStrokes?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingMaskRef.current = true;
|
|
||||||
|
|
||||||
const offscreenCanvas = new OffscreenCanvas(
|
|
||||||
overlayCanvasRef.current!.width,
|
|
||||||
overlayCanvasRef.current!.height
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loadMaksWorkerRef.current) {
|
|
||||||
loadMaksWorkerRef.current = new Worker(
|
|
||||||
new URL('./offscreen-worker.ts', import.meta.url),
|
|
||||||
{
|
|
||||||
type: 'module'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMaksWorkerRef.current!.onmessage = (event: any) => {
|
|
||||||
if (event.data.type === 'done' && event.data.imageData) {
|
|
||||||
console.log('load mask done');
|
|
||||||
// draw the data to the overlay canvas
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d')!;
|
|
||||||
ctx.putImageData(event.data.imageData, 0, 0);
|
|
||||||
|
|
||||||
isLoadingMaskRef.current = false;
|
|
||||||
saveImage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// send offscreen canvas to worker
|
|
||||||
loadMaksWorkerRef.current?.postMessage(
|
|
||||||
{ canvas: offscreenCanvas, type: 'init' },
|
|
||||||
[offscreenCanvas]
|
|
||||||
);
|
|
||||||
|
|
||||||
// send draw data to worker
|
|
||||||
loadMaksWorkerRef.current?.postMessage({
|
|
||||||
type: 'draw',
|
|
||||||
maskStrokes: maskStrokes,
|
|
||||||
strokes: mainStrokes || []
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('error---', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const redrawStrokes = async (strokes: Stroke[]) => {
|
|
||||||
if (!strokes.length && !maskStorkeRef.current.length) {
|
|
||||||
clearOverlayCanvas();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMaskPixs(maskStorkeRef.current, strokes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const undo = useCallback(() => {
|
|
||||||
if (
|
|
||||||
strokesRef.current.length === 0 &&
|
|
||||||
maskStorkeRef.current.length === 0
|
|
||||||
) {
|
|
||||||
clearOverlayCanvas();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastlength = strokesRef.current.length;
|
|
||||||
|
|
||||||
const newStrokes = strokesRef.current.slice(0, -1);
|
|
||||||
setStrokes(newStrokes);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!newStrokes.length &&
|
|
||||||
lastlength === 0 &&
|
|
||||||
maskStorkeRef.current.length
|
|
||||||
) {
|
|
||||||
setMaskStrokes([]);
|
|
||||||
}
|
|
||||||
clearTimeout(timer.current);
|
|
||||||
timer.current = setTimeout(() => {
|
|
||||||
redrawStrokes(newStrokes);
|
|
||||||
}, 100);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const downloadOriginImage = () => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = `image_${dayjs().format('YYYYMMDDHHmmss')}.png`;
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadNewImage = () => {
|
|
||||||
if (!imageSrc) return;
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.src = imageSrc;
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = imageStatus.width;
|
|
||||||
canvas.height = imageStatus.height;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
const url = canvas.toDataURL('image/png');
|
|
||||||
const filename = `${canvas.width}x${canvas.height}_${dayjs().format('YYYYMMDDHHmmss')}.png`;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const download = () => {
|
|
||||||
if (imageStatus.isOriginal) {
|
|
||||||
downloadOriginImage();
|
|
||||||
} else {
|
|
||||||
downloadNewImage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawImage = useCallback(async () => {
|
|
||||||
if (!containerRef.current || !canvasRef.current) return;
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = imageSrc;
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const ctx = canvas!.getContext('2d');
|
|
||||||
const container = containerRef.current;
|
|
||||||
baseScale.current = Math.min(
|
|
||||||
container!.offsetWidth / img.width,
|
|
||||||
container!.offsetHeight / img.height,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
// if need to fit the image to the container, show * baseScale.current
|
|
||||||
canvas!.width = img.width;
|
|
||||||
canvas!.height = img.height;
|
|
||||||
|
|
||||||
// fit the image to the container
|
|
||||||
autoScale.current = 1;
|
|
||||||
creatOffscreenCanvas();
|
|
||||||
updateCanvasSize();
|
|
||||||
clearCanvas();
|
|
||||||
|
|
||||||
ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [imageSrc]);
|
|
||||||
|
|
||||||
const invertPainting = (isChecked: boolean) => {
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d');
|
|
||||||
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
const canvasWidth = overlayCanvasRef.current!.width;
|
|
||||||
const canvasHeight = overlayCanvasRef.current!.height;
|
|
||||||
|
|
||||||
clearOverlayCanvas();
|
|
||||||
|
|
||||||
const offscreenCanvas = new OffscreenCanvas(
|
|
||||||
overlayCanvasRef.current!.width,
|
|
||||||
overlayCanvasRef.current!.height
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!invertWorkerRef.current) {
|
|
||||||
invertWorkerRef.current = new Worker(
|
|
||||||
new URL('./invert-worker.ts', import.meta.url),
|
|
||||||
{
|
|
||||||
type: 'module'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
invertWorkerRef.current.onmessage = (event: any) => {
|
|
||||||
if (event.data.type === 'done' && event.data.imageData) {
|
|
||||||
// draw the data to the overlay canvas
|
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d')!;
|
|
||||||
ctx.putImageData(event.data.imageData, 0, 0);
|
|
||||||
|
|
||||||
isLoadingMaskRef.current = false;
|
|
||||||
saveImage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// send offscreen canvas to worker
|
|
||||||
invertWorkerRef.current?.postMessage(
|
|
||||||
{ canvas: offscreenCanvas, type: 'init' },
|
|
||||||
[offscreenCanvas]
|
|
||||||
);
|
|
||||||
|
|
||||||
// send draw data to worker
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
|
|
||||||
invertWorkerRef.current?.postMessage({
|
|
||||||
type: 'draw',
|
|
||||||
width: imageData.width,
|
|
||||||
height: imageData.height,
|
|
||||||
strokes: strokesRef.current,
|
|
||||||
maskStrokes: maskStorkeRef.current
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
redrawStrokes(strokesRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCursorSize = () => {
|
|
||||||
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
|
|
||||||
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeImage = useCallback(async () => {
|
|
||||||
await drawImage();
|
|
||||||
onScaleImageSize?.({
|
|
||||||
width: canvasRef.current!.width,
|
|
||||||
height: canvasRef.current!.height
|
|
||||||
});
|
|
||||||
|
|
||||||
if (imageStatus.isResetNeeded) {
|
|
||||||
onReset();
|
|
||||||
resetCanvas();
|
|
||||||
} else if (
|
|
||||||
(strokesRef.current.length || maskStorkeRef.current.length) &&
|
|
||||||
imageStatus.isOriginal &&
|
|
||||||
!negativeMaskRef.current
|
|
||||||
) {
|
|
||||||
redrawStrokes(strokesRef.current);
|
|
||||||
} else if (
|
|
||||||
(strokesRef.current.length || maskStorkeRef.current.length) &&
|
|
||||||
imageStatus.isOriginal &&
|
|
||||||
negativeMaskRef.current
|
|
||||||
) {
|
|
||||||
invertPainting(true);
|
|
||||||
}
|
|
||||||
console.log('Image status:', imageStatus, negativeMaskRef.current);
|
|
||||||
|
|
||||||
updateCursorSize();
|
|
||||||
}, [drawImage, imageStatus.isOriginal, imageStatus.isResetNeeded]);
|
|
||||||
|
|
||||||
const handleFitView = () => {
|
|
||||||
fitView();
|
|
||||||
setActiveScale(autoScale.current);
|
|
||||||
updateCursorSize();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBrushSizeChange = (value: number) => {
|
|
||||||
setLineWidth(value);
|
|
||||||
cursorRef.current!.style.width = `${value}px`;
|
|
||||||
cursorRef.current!.style.height = `${value}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnChangeMask = (e: any) => {
|
|
||||||
negativeMaskRef.current = e.target.checked;
|
|
||||||
invertPainting(e.target.checked);
|
|
||||||
setInvertMask(e.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initializeImage();
|
|
||||||
}, [initializeImage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleUndoShortcut = (e: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
(e.ctrlKey || e.metaKey) &&
|
|
||||||
e.key === 'z' &&
|
|
||||||
!negativeMaskRef.current
|
|
||||||
) {
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleUndoShortcut);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleUndoShortcut);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
mouseDownState.current = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
|
||||||
mouseDownState.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// mouse down
|
|
||||||
window.addEventListener('mousedown', handleMouseDown);
|
|
||||||
|
|
||||||
// mouse up
|
|
||||||
window.addEventListener('mouseup', handleMouseUp);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer.current);
|
|
||||||
|
|
||||||
window.removeEventListener('mousedown', handleMouseDown);
|
|
||||||
window.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteMask = () => {
|
|
||||||
setInvertMask(false);
|
|
||||||
redrawStrokes(strokesRef.current);
|
|
||||||
saveImage();
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
clearMask: handleDeleteMask,
|
|
||||||
loadMaskPixs: async function (strokes: Stroke[]) {
|
|
||||||
setMaskStrokes(strokes);
|
|
||||||
setStrokes([]);
|
|
||||||
setTransform();
|
|
||||||
clearOverlayCanvas();
|
|
||||||
loadMaskPixs(strokes);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invertWorkerRef.current = new Worker(
|
|
||||||
new URL('./invert-worker.ts', import.meta.url),
|
|
||||||
{
|
|
||||||
type: 'module'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
loadMaksWorkerRef.current = new Worker(
|
|
||||||
new URL('./offscreen-worker.ts', import.meta.url),
|
|
||||||
{
|
|
||||||
type: 'module'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
invertWorkerRef.current?.terminate();
|
|
||||||
loadMaksWorkerRef.current?.terminate();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (maskUpload?.length) {
|
|
||||||
// clear the overlay canvas
|
|
||||||
clearOverlayCanvas();
|
|
||||||
}
|
|
||||||
}, [maskUpload]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (disabled && cursorRef.current) {
|
|
||||||
cursorRef.current!.style.display = 'none';
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="editor-wrapper">
|
|
||||||
<div className="flex-between">
|
|
||||||
<ToolsBar
|
|
||||||
handleBrushSizeChange={handleBrushSizeChange}
|
|
||||||
undo={undo}
|
|
||||||
onClear={onReset}
|
|
||||||
handleFitView={handleFitView}
|
|
||||||
handleUpdateImageList={handleUpdateImageList}
|
|
||||||
handleUpdateMaskList={handleUpdateMaskList}
|
|
||||||
disabled={disabled}
|
|
||||||
lineWidth={lineWidth}
|
|
||||||
invertMask={invertMask}
|
|
||||||
loading={loading}
|
|
||||||
accept={accept}
|
|
||||||
></ToolsBar>
|
|
||||||
|
|
||||||
<ImageActionsBar
|
|
||||||
handleOnChangeMask={handleOnChangeMask}
|
|
||||||
invertMask={invertMask}
|
|
||||||
downloadMask={downloadMask}
|
|
||||||
download={download}
|
|
||||||
isOriginal={imageStatus.isOriginal}
|
|
||||||
disabled={isDisabled || !!maskUpload?.length}
|
|
||||||
maskUpload={maskUpload}
|
|
||||||
></ImageActionsBar>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="editor-content"
|
|
||||||
ref={containerRef}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
flex: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loadingSize.loading && (
|
|
||||||
<Loading
|
|
||||||
width={loadingSize.width}
|
|
||||||
height={loadingSize.height}
|
|
||||||
></Loading>
|
|
||||||
)}
|
|
||||||
<canvas ref={canvasRef} style={{ position: 'absolute', zIndex: 1 }} />
|
|
||||||
<canvas
|
|
||||||
ref={overlayCanvasRef}
|
|
||||||
className={classNames('overlay-canvas', {
|
|
||||||
'overlay-canvas--disabled': disabled
|
|
||||||
})}
|
|
||||||
style={{ position: 'absolute', zIndex: 10, cursor: 'none' }}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
if (isLoadingMaskRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mouseDownState.current = true;
|
|
||||||
startDrawing(event);
|
|
||||||
}}
|
|
||||||
onMouseUp={(event) => {
|
|
||||||
if (isLoadingMaskRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mouseDownState.current = false;
|
|
||||||
endDrawing(event);
|
|
||||||
}}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onWheel={handleOnWheel}
|
|
||||||
onMouseMove={(e) => {
|
|
||||||
if (isLoadingMaskRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleMouseMove(e);
|
|
||||||
draw(e);
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (isLoadingMaskRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
endDrawing(e);
|
|
||||||
handleMouseLeave();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={cursorRef}
|
|
||||||
style={{
|
|
||||||
display: 'none',
|
|
||||||
position: 'fixed',
|
|
||||||
width: lineWidth * activeScale,
|
|
||||||
height: lineWidth * activeScale,
|
|
||||||
backgroundColor: COLOR,
|
|
||||||
borderRadius: '50%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
cursor: 'none',
|
|
||||||
zIndex: 5
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default React.memo(CanvasImageEditor);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/// <reference lib="webworker" />
|
|
||||||
|
|
||||||
let offscreenCanvas: OffscreenCanvas;
|
|
||||||
let ctx: OffscreenCanvasRenderingContext2D;
|
|
||||||
|
|
||||||
const COLOR = 'rgba(0, 0, 255, 0.3)';
|
|
||||||
|
|
||||||
type Point = { x: number; y: number; lineWidth: number };
|
|
||||||
type Stroke = Point[];
|
|
||||||
|
|
||||||
self.onmessage = (event) => {
|
|
||||||
const { width, height, strokes, maskStrokes } = event.data;
|
|
||||||
|
|
||||||
if (event.data.type === 'init') {
|
|
||||||
offscreenCanvas = event.data.canvas;
|
|
||||||
ctx = offscreenCanvas!.getContext('2d')!;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.type === 'draw') {
|
|
||||||
ctx.fillStyle = COLOR;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
ctx.globalCompositeOperation = 'destination-out';
|
|
||||||
|
|
||||||
[...strokes].forEach((stroke: Stroke) => {
|
|
||||||
stroke.forEach((point: { x: number; y: number; lineWidth: number }) => {
|
|
||||||
const lineWidth = point.lineWidth;
|
|
||||||
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// points
|
|
||||||
maskStrokes?.forEach((stroke: Stroke) => {
|
|
||||||
stroke.forEach((point: { x: number; y: number; lineWidth: number }) => {
|
|
||||||
const lineWidth = 4;
|
|
||||||
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const newImageData = ctx.getImageData(0, 0, width, height);
|
|
||||||
self.postMessage({ type: 'done', imageData: newImageData }, [
|
|
||||||
newImageData.data.buffer
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
let offscreenCanvas: OffscreenCanvas;
|
|
||||||
let ctx: OffscreenCanvasRenderingContext2D;
|
|
||||||
|
|
||||||
const COLOR = 'rgba(0, 0, 255, 0.3)';
|
|
||||||
|
|
||||||
type Point = { x: number; y: number; lineWidth: number };
|
|
||||||
type Stroke = Point[];
|
|
||||||
|
|
||||||
const postDone = () => {
|
|
||||||
const imageData = ctx.getImageData(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
offscreenCanvas.width,
|
|
||||||
offscreenCanvas.height
|
|
||||||
);
|
|
||||||
self.postMessage({ type: 'done', imageData });
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawFillRect = (
|
|
||||||
ctx: OffscreenCanvasRenderingContext2D,
|
|
||||||
stroke: Stroke,
|
|
||||||
options: any
|
|
||||||
) => {
|
|
||||||
const { color } = options;
|
|
||||||
|
|
||||||
stroke?.forEach(({ x, y }) => {
|
|
||||||
const width = options.lineWidth || 10;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
||||||
ctx.globalCompositeOperation = 'destination-out';
|
|
||||||
ctx.fillRect(x - width / 2, y - width / 2, width, width);
|
|
||||||
|
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(x - width / 2, y - width / 2, width, width);
|
|
||||||
ctx.restore();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawStrokes = (strokes: Stroke[]) => {
|
|
||||||
strokes?.forEach((stroke) => {
|
|
||||||
drawFillRect(ctx, stroke, {
|
|
||||||
color: COLOR
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawLine = (
|
|
||||||
stroke: Stroke | Point[],
|
|
||||||
options: {
|
|
||||||
lineWidth?: number;
|
|
||||||
color: string;
|
|
||||||
compositeOperation: 'source-over' | 'destination-out';
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { color, compositeOperation } = options;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.globalCompositeOperation = compositeOperation;
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
|
|
||||||
stroke?.forEach((point, i) => {
|
|
||||||
const { x, y } = point;
|
|
||||||
ctx.lineWidth = point.lineWidth || 10;
|
|
||||||
if (i === 0) {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (compositeOperation === 'source-over') {
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawLines = (strokes: Stroke[]) => {
|
|
||||||
strokes?.forEach((stroke: Point[], index) => {
|
|
||||||
drawLine(stroke, {
|
|
||||||
color: COLOR,
|
|
||||||
compositeOperation: 'destination-out'
|
|
||||||
});
|
|
||||||
|
|
||||||
drawLine(stroke, {
|
|
||||||
color: COLOR,
|
|
||||||
compositeOperation: 'source-over'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
self.onmessage = (event) => {
|
|
||||||
if (event.data.type === 'init') {
|
|
||||||
offscreenCanvas = event.data.canvas;
|
|
||||||
ctx = offscreenCanvas!.getContext('2d')!;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.type === 'draw') {
|
|
||||||
ctx?.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
|
||||||
const { maskStrokes, strokes } = event.data;
|
|
||||||
drawStrokes(maskStrokes);
|
|
||||||
drawLines(strokes);
|
|
||||||
postDone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
|
||||||
import { KeyMap } from '@/config/hotkeys';
|
|
||||||
import UploadImg from '@/pages/playground/components/upload-img';
|
|
||||||
import {
|
|
||||||
ClearOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
ExpandOutlined,
|
|
||||||
FormatPainterOutlined,
|
|
||||||
UndoOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Checkbox, Slider, Tooltip } from 'antd';
|
|
||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface ToolsBarProps {
|
|
||||||
disabled: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
lineWidth: number;
|
|
||||||
uploadButton?: React.ReactNode;
|
|
||||||
invertMask?: boolean;
|
|
||||||
accept?: string;
|
|
||||||
handleBrushSizeChange: (value: number) => void;
|
|
||||||
undo: () => void;
|
|
||||||
onClear: () => void;
|
|
||||||
handleFitView: () => void;
|
|
||||||
handleUpdateImageList: (fileList: any[]) => void;
|
|
||||||
handleUpdateMaskList: (fileList: any[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
|
||||||
const {
|
|
||||||
disabled,
|
|
||||||
loading,
|
|
||||||
lineWidth,
|
|
||||||
invertMask,
|
|
||||||
accept,
|
|
||||||
handleBrushSizeChange,
|
|
||||||
undo,
|
|
||||||
onClear,
|
|
||||||
handleFitView,
|
|
||||||
handleUpdateImageList,
|
|
||||||
handleUpdateMaskList
|
|
||||||
} = props;
|
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
|
||||||
<div className="tools">
|
|
||||||
<Tooltip
|
|
||||||
placement="bottomLeft"
|
|
||||||
arrow={false}
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
background: 'var(--color-white-1)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
width: 160
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
<div className="flex-column" style={{ width: '100%' }}>
|
|
||||||
<span className="text-secondary">
|
|
||||||
{intl.formatMessage({ id: 'playground.image.brushSize' })}
|
|
||||||
</span>
|
|
||||||
<Slider
|
|
||||||
disabled={disabled}
|
|
||||||
style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }}
|
|
||||||
vertical={false}
|
|
||||||
defaultValue={lineWidth}
|
|
||||||
min={10}
|
|
||||||
max={100}
|
|
||||||
onChange={handleBrushSizeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button size="middle" type="text">
|
|
||||||
<FormatPainterOutlined className="font-size-14" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<span>
|
|
||||||
[{KeyMap.UNDO.textKeybinding}]
|
|
||||||
<span className="m-l-5">
|
|
||||||
{intl.formatMessage({ id: 'common.button.undo' })}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button onClick={undo} size="middle" type="text" disabled={disabled}>
|
|
||||||
<UndoOutlined className="font-size-14" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
|
|
||||||
<Button onClick={onClear} size="middle" type="text" disabled={disabled}>
|
|
||||||
<ClearOutlined className="font-size-14" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<UploadImg
|
|
||||||
disabled={loading || invertMask}
|
|
||||||
handleUpdateImgList={handleUpdateImageList}
|
|
||||||
size="middle"
|
|
||||||
accept={accept}
|
|
||||||
></UploadImg>
|
|
||||||
<UploadImg
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: 'playground.image.mask.upload'
|
|
||||||
})}
|
|
||||||
icon={<IconFont type="icon-mosaic-2"></IconFont>}
|
|
||||||
disabled={loading || invertMask}
|
|
||||||
handleUpdateImgList={handleUpdateMaskList}
|
|
||||||
size="middle"
|
|
||||||
accept={accept}
|
|
||||||
></UploadImg>
|
|
||||||
<Tooltip title={intl.formatMessage({ id: 'playground.image.fitview' })}>
|
|
||||||
<Button
|
|
||||||
onClick={handleFitView}
|
|
||||||
size="middle"
|
|
||||||
type="text"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<ExpandOutlined className="font-size-14" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ImageActionsBarProps {
|
|
||||||
disabled: boolean;
|
|
||||||
maskUpload?: any[];
|
|
||||||
isOriginal: boolean;
|
|
||||||
invertMask: boolean;
|
|
||||||
handleOnChangeMask: (e: CheckboxChangeEvent) => void;
|
|
||||||
downloadMask: () => void;
|
|
||||||
download: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageActionsBar: React.FC<ImageActionsBarProps> = (props) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
|
||||||
disabled,
|
|
||||||
isOriginal,
|
|
||||||
invertMask,
|
|
||||||
handleOnChangeMask,
|
|
||||||
downloadMask,
|
|
||||||
download
|
|
||||||
} = props;
|
|
||||||
return (
|
|
||||||
<div className="tools">
|
|
||||||
{isOriginal && (
|
|
||||||
<>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<span style={{ whiteSpace: 'pre-wrap' }}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: 'playground.image.negativeMask.tips'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
onChange={handleOnChangeMask}
|
|
||||||
className="flex-center"
|
|
||||||
checked={invertMask}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<span className="font-size-12">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: 'playground.image.negativeMask'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Checkbox>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: 'playground.image.saveMask'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Button onClick={downloadMask} size="middle" type="text">
|
|
||||||
<IconFont className="font-size-14" type="icon-save1"></IconFont>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isOriginal && (
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: 'playground.image.download' })}
|
|
||||||
>
|
|
||||||
<Button onClick={download} size="middle" type="text">
|
|
||||||
<DownloadOutlined className="font-size-14" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ImageActionsBar, ToolsBar };
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import AutoTooltip from '@/components/auto-tooltip';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const LabelsWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface LabelCellProps {
|
|
||||||
labels: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LabelsCell: React.FC<LabelCellProps> = ({ labels }) => (
|
|
||||||
<LabelsWrapper>
|
|
||||||
{_.map(labels, (value: string, key: string) => (
|
|
||||||
<AutoTooltip
|
|
||||||
key={key}
|
|
||||||
className="m-r-0"
|
|
||||||
maxWidth={155}
|
|
||||||
style={{ paddingInline: 8, borderRadius: 12 }}
|
|
||||||
>
|
|
||||||
<span>{key}</span>
|
|
||||||
<span>:{value}</span>
|
|
||||||
</AutoTooltip>
|
|
||||||
))}
|
|
||||||
</LabelsWrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default LabelsCell;
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import AutoComplete from '@/components/seal-form/auto-complete';
|
|
||||||
import { MinusOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import { useLabelSelectorContext } from './context';
|
|
||||||
import './styles/label-item.less';
|
|
||||||
interface LabelItemProps {
|
|
||||||
label: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
labels?: Record<string, any>;
|
|
||||||
labelKey?: string;
|
|
||||||
labelValue?: string;
|
|
||||||
keyAddon?: React.ReactNode;
|
|
||||||
valueAddon?: React.ReactNode;
|
|
||||||
seperator?: string;
|
|
||||||
labelList: { key: string; value: string }[];
|
|
||||||
disabled?: boolean;
|
|
||||||
onDelete?: () => void;
|
|
||||||
onChange?: (params: { key: string; value: string }) => void;
|
|
||||||
onPaste?: (e: any) => void;
|
|
||||||
onBlur?: (e: any, type: string) => void;
|
|
||||||
}
|
|
||||||
const LabelItem: React.FC<LabelItemProps> = ({
|
|
||||||
labels,
|
|
||||||
label,
|
|
||||||
labelList,
|
|
||||||
seperator,
|
|
||||||
keyAddon,
|
|
||||||
valueAddon,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
onDelete,
|
|
||||||
onBlur
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { options, placeholder = [] } = useLabelSelectorContext();
|
|
||||||
|
|
||||||
const keyOptions = useMemo(() => {
|
|
||||||
return options?.filter(
|
|
||||||
(item) => !_.has(labels, item.value) || item.value === label.key
|
|
||||||
);
|
|
||||||
}, [labels, options]);
|
|
||||||
|
|
||||||
const valueOptions = useMemo(() => {
|
|
||||||
return options?.find((item) => item.value === label.key)?.children || [];
|
|
||||||
}, [label.key, options]);
|
|
||||||
|
|
||||||
const handleOnValueChange = (value: string) => {
|
|
||||||
onChange?.({
|
|
||||||
key: label.key,
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnKeyChange = (key: any) => {
|
|
||||||
onChange?.({
|
|
||||||
key,
|
|
||||||
value: label.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyOnBlur = (e: any, type: string) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
// has duplicate key
|
|
||||||
const duplicates = _.filter(
|
|
||||||
labelList,
|
|
||||||
(item: Global.BaseListItem<string>) => val && val === item.key
|
|
||||||
);
|
|
||||||
if (duplicates.length > 1) {
|
|
||||||
setOpen(true);
|
|
||||||
onChange?.({
|
|
||||||
key: '',
|
|
||||||
value: label.value
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
onBlur?.(e, type);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="label-item">
|
|
||||||
<div className="label-key">
|
|
||||||
{keyAddon ?? (
|
|
||||||
<Tooltip
|
|
||||||
open={open}
|
|
||||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
|
||||||
>
|
|
||||||
<AutoComplete
|
|
||||||
options={keyOptions}
|
|
||||||
disabled={disabled}
|
|
||||||
checkStatus="success"
|
|
||||||
label={
|
|
||||||
placeholder?.[0] ||
|
|
||||||
intl.formatMessage({ id: 'common.input.key' })
|
|
||||||
}
|
|
||||||
value={label.key}
|
|
||||||
onChange={handleOnKeyChange}
|
|
||||||
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
|
|
||||||
></AutoComplete>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{seperator && <span className="seprator">{seperator}</span>}
|
|
||||||
<div className="label-value">
|
|
||||||
{valueAddon ?? (
|
|
||||||
<AutoComplete
|
|
||||||
options={valueOptions}
|
|
||||||
disabled={disabled}
|
|
||||||
checkStatus={label.value ? 'success' : ''}
|
|
||||||
label={
|
|
||||||
placeholder?.[1] ||
|
|
||||||
intl.formatMessage({ id: 'common.input.value' })
|
|
||||||
}
|
|
||||||
value={label.value}
|
|
||||||
onChange={handleOnValueChange}
|
|
||||||
onBlur={(e: any) => onBlur?.(e, 'value')}
|
|
||||||
></AutoComplete>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!disabled && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
className="btn"
|
|
||||||
type="default"
|
|
||||||
shape="circle"
|
|
||||||
onClick={onDelete}
|
|
||||||
>
|
|
||||||
<MinusOutlined />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabelItem;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface LabelSelectorContextProps {
|
|
||||||
options?: Array<{
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
children?: { label: string; value: string | number }[];
|
|
||||||
}>;
|
|
||||||
placeholder?: string[];
|
|
||||||
currentData?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LabelSelectorContext =
|
|
||||||
React.createContext<LabelSelectorContextProps>(
|
|
||||||
{} as LabelSelectorContextProps
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useLabelSelectorContext = () => {
|
|
||||||
const context = React.useContext(LabelSelectorContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useLabelSelectorContext must be used within a LabelSelectorProvider'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import Inner from './inner';
|
|
||||||
|
|
||||||
interface LabelSelectorProps {
|
|
||||||
labels: Record<string, any>;
|
|
||||||
label?: string;
|
|
||||||
btnText?: string;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
isAutoComplete?: boolean;
|
|
||||||
enablePaste?: boolean;
|
|
||||||
onChange?: (labels: Record<string, any>) => void;
|
|
||||||
onBlur?: (e: any, type: string, index: number) => void;
|
|
||||||
onDelete?: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LabelSelector: React.FC<LabelSelectorProps> = ({
|
|
||||||
labels,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
onDelete,
|
|
||||||
disabled,
|
|
||||||
label,
|
|
||||||
btnText,
|
|
||||||
description,
|
|
||||||
isAutoComplete,
|
|
||||||
enablePaste = true
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [labelsData, setLabelsData] = useState({});
|
|
||||||
const [labelList, setLabelList] = useState<{ key: string; value: string }[]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!_.isEqual(labels, labelsData)) {
|
|
||||||
setLabelsData(labels || {});
|
|
||||||
const list = _.map(_.keys(labels), (key: string) => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: labels[key]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setLabelList(list);
|
|
||||||
}
|
|
||||||
}, [labels]);
|
|
||||||
|
|
||||||
const handleLabelListChange = (list: { key: string; value: string }[]) => {
|
|
||||||
setLabelList(list);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLabelsChange = (data: Record<string, any>) => {
|
|
||||||
console.log('handleLabelsChange', data);
|
|
||||||
setLabelsData(data);
|
|
||||||
onChange?.(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateLabels = (list: { key: string; value: string }[]) => {
|
|
||||||
const newLabels = _.reduce(
|
|
||||||
list,
|
|
||||||
(result: any, item: any) => {
|
|
||||||
if (item.key) {
|
|
||||||
result[item.key] = item.value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
onChange?.(newLabels);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnPaste = (
|
|
||||||
e: React.ClipboardEvent<HTMLTextAreaElement>,
|
|
||||||
index: number
|
|
||||||
) => {
|
|
||||||
if (!enablePaste) return;
|
|
||||||
const clipboardText = e.clipboardData.getData('text');
|
|
||||||
if (!clipboardText || clipboardText.indexOf('=') === -1) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const lines = _.split(clipboardText, /\r?\n/)
|
|
||||||
.map((line: string) => line.trim())
|
|
||||||
.filter((line: string) => line && line.includes('='));
|
|
||||||
|
|
||||||
const parsedData = lines.map((line: string) => {
|
|
||||||
const [key, value] = line.split(/=(.+)/).map((s) => s.trim());
|
|
||||||
return { key, value };
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPairs = [...labelList];
|
|
||||||
newPairs.splice(index, 1, ...parsedData);
|
|
||||||
|
|
||||||
updateLabels(newPairs);
|
|
||||||
setLabelList(newPairs);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Inner
|
|
||||||
disabled={disabled}
|
|
||||||
label={label}
|
|
||||||
btnText={btnText}
|
|
||||||
description={
|
|
||||||
description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' })
|
|
||||||
}
|
|
||||||
isAutoComplete={isAutoComplete}
|
|
||||||
labels={labelsData}
|
|
||||||
labelList={labelList}
|
|
||||||
onChange={handleLabelsChange}
|
|
||||||
onLabelListChange={handleLabelListChange}
|
|
||||||
onPaste={handleOnPaste}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabelSelector;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import AutoCompleteItem from './autocomplete-item';
|
|
||||||
import LabelItem from './label-item';
|
|
||||||
import Wrapper from './wrapper';
|
|
||||||
interface LabelSelectorProps {
|
|
||||||
labels: Record<string, any>;
|
|
||||||
label?: string;
|
|
||||||
btnText?: string;
|
|
||||||
isAutoComplete?: boolean;
|
|
||||||
labelList: Array<{ key: string; value: string }>;
|
|
||||||
onLabelListChange: (list: { key: string; value: string }[]) => void;
|
|
||||||
onChange?: (labels: Record<string, any>) => void;
|
|
||||||
onPaste?: (e: any, index: number) => void;
|
|
||||||
onBlur?: (e: any, type: string, index: number) => void;
|
|
||||||
onDelete?: (index: number) => void;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Inner: React.FC<LabelSelectorProps> = ({
|
|
||||||
labels,
|
|
||||||
labelList,
|
|
||||||
onChange,
|
|
||||||
onLabelListChange,
|
|
||||||
onPaste,
|
|
||||||
onBlur,
|
|
||||||
onDelete,
|
|
||||||
disabled,
|
|
||||||
label,
|
|
||||||
btnText,
|
|
||||||
description,
|
|
||||||
isAutoComplete
|
|
||||||
}) => {
|
|
||||||
const updateLabels = (list: { key: string; value: string }[]) => {
|
|
||||||
const newLabels = _.reduce(
|
|
||||||
list,
|
|
||||||
(result: any, item: any) => {
|
|
||||||
if (item.key) {
|
|
||||||
result[item.key] = item.value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
onChange?.(newLabels);
|
|
||||||
};
|
|
||||||
const handleOnChange = (index: number, label: any) => {
|
|
||||||
const list = _.cloneDeep(labelList);
|
|
||||||
list[index] = label;
|
|
||||||
onLabelListChange(list);
|
|
||||||
updateLabels(list);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddLabel = () => {
|
|
||||||
const newLabelList = [
|
|
||||||
...labelList,
|
|
||||||
{
|
|
||||||
key: '',
|
|
||||||
value: ''
|
|
||||||
}
|
|
||||||
];
|
|
||||||
onLabelListChange(newLabelList);
|
|
||||||
updateLabels(newLabelList);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnDelete = (index: number) => {
|
|
||||||
const list = _.cloneDeep(labelList);
|
|
||||||
list.splice(index, 1);
|
|
||||||
onLabelListChange(list);
|
|
||||||
updateLabels(list);
|
|
||||||
onDelete?.(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
label={label}
|
|
||||||
description={description}
|
|
||||||
onAdd={handleAddLabel}
|
|
||||||
disabled={disabled}
|
|
||||||
btnText={btnText}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{isAutoComplete
|
|
||||||
? labelList?.map((item: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<AutoCompleteItem
|
|
||||||
disabled={disabled}
|
|
||||||
key={index}
|
|
||||||
label={item}
|
|
||||||
seperator=":"
|
|
||||||
labels={labels}
|
|
||||||
labelList={labelList}
|
|
||||||
onDelete={() => handleOnDelete(index)}
|
|
||||||
onChange={(obj) => handleOnChange(index, obj)}
|
|
||||||
onPaste={(e) => onPaste?.(e, index)}
|
|
||||||
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: labelList?.map((item: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<LabelItem
|
|
||||||
disabled={disabled}
|
|
||||||
key={index}
|
|
||||||
label={item}
|
|
||||||
seperator=":"
|
|
||||||
labelList={labelList}
|
|
||||||
onDelete={() => handleOnDelete(index)}
|
|
||||||
onChange={(obj) => handleOnChange(index, obj)}
|
|
||||||
onPaste={(e) => onPaste?.(e, index)}
|
|
||||||
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Inner;
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import SealInput from '@/components/seal-form/seal-input';
|
|
||||||
import { MinusOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import './styles/label-item.less';
|
|
||||||
interface LabelItemProps {
|
|
||||||
label: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
labelKey?: string;
|
|
||||||
labelValue?: string;
|
|
||||||
keyAddon?: React.ReactNode;
|
|
||||||
valueAddon?: React.ReactNode;
|
|
||||||
seperator?: string;
|
|
||||||
labelList: { key: string; value: string }[];
|
|
||||||
disabled?: boolean;
|
|
||||||
onDelete?: () => void;
|
|
||||||
onChange?: (params: { key: string; value: string }) => void;
|
|
||||||
onPaste?: (e: any) => void;
|
|
||||||
onBlur?: (e: any, type: string) => void;
|
|
||||||
}
|
|
||||||
const LabelItem: React.FC<LabelItemProps> = ({
|
|
||||||
label,
|
|
||||||
labelList,
|
|
||||||
seperator,
|
|
||||||
keyAddon,
|
|
||||||
valueAddon,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
onDelete,
|
|
||||||
onPaste,
|
|
||||||
onBlur
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleOnValueChange = (e: any) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
onChange?.({
|
|
||||||
key: label.key,
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnKeyChange = (e: any) => {
|
|
||||||
const key = e.target.value;
|
|
||||||
onChange?.({
|
|
||||||
key,
|
|
||||||
value: label.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyOnBlur = (e: any, type: string) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
// has duplicate key
|
|
||||||
const duplicates = _.filter(
|
|
||||||
labelList,
|
|
||||||
(item: Global.BaseListItem<string>) => val && val === item.key
|
|
||||||
);
|
|
||||||
if (duplicates.length > 1) {
|
|
||||||
setOpen(true);
|
|
||||||
onChange?.({
|
|
||||||
key: '',
|
|
||||||
value: label.value
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
onBlur?.(e, type);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="label-item">
|
|
||||||
<div className="label-key">
|
|
||||||
{keyAddon ?? (
|
|
||||||
<Tooltip
|
|
||||||
open={open}
|
|
||||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<SealInput.Input
|
|
||||||
disabled={disabled}
|
|
||||||
checkStatus="success"
|
|
||||||
label={intl.formatMessage({ id: 'common.input.key' })}
|
|
||||||
value={label.key}
|
|
||||||
onChange={handleOnKeyChange}
|
|
||||||
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
|
|
||||||
onPaste={onPaste}
|
|
||||||
></SealInput.Input>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{seperator && <span className="seprator">{seperator}</span>}
|
|
||||||
<div className="label-value">
|
|
||||||
{valueAddon ?? (
|
|
||||||
<SealInput.Input
|
|
||||||
trim={false}
|
|
||||||
disabled={disabled}
|
|
||||||
checkStatus={label.value ? 'success' : ''}
|
|
||||||
label={intl.formatMessage({ id: 'common.input.value' })}
|
|
||||||
value={label.value}
|
|
||||||
onChange={handleOnValueChange}
|
|
||||||
onBlur={(e: any) => onBlur?.(e, 'value')}
|
|
||||||
></SealInput.Input>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!disabled && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
className="btn"
|
|
||||||
type="default"
|
|
||||||
shape="circle"
|
|
||||||
onClick={onDelete}
|
|
||||||
>
|
|
||||||
<MinusOutlined />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabelItem;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.label-item {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
.seprator {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
width: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 24px;
|
|
||||||
margin-left: 10px;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-key {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-value {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import LabelInfo from '@/components/seal-form/components/label-info';
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
interface WrapperProps {
|
|
||||||
required?: boolean;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
labelExtra?: React.ReactNode;
|
|
||||||
children: React.ReactNode;
|
|
||||||
btnText?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onAdd?: () => void;
|
|
||||||
styles?: {
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
button?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
position: relative;
|
|
||||||
padding: 14px;
|
|
||||||
padding-top: 34px;
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
border-radius: var(--ant-border-radius-lg);
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
top: 12px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonWrapper = styled.div`
|
|
||||||
margin-top: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper: React.FC<WrapperProps> = ({
|
|
||||||
required,
|
|
||||||
children,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
labelExtra,
|
|
||||||
onAdd,
|
|
||||||
btnText,
|
|
||||||
disabled,
|
|
||||||
button,
|
|
||||||
styles
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
|
||||||
<Container style={styles?.wrapper}>
|
|
||||||
{label && (
|
|
||||||
<span className="label">
|
|
||||||
<LabelInfo
|
|
||||||
required={required}
|
|
||||||
label={label}
|
|
||||||
description={description}
|
|
||||||
labelExtra={labelExtra}
|
|
||||||
></LabelInfo>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
{!disabled && (
|
|
||||||
<ButtonWrapper>
|
|
||||||
{button || (
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
color="default"
|
|
||||||
block
|
|
||||||
onClick={onAdd}
|
|
||||||
style={{ borderRadius: 'var(--border-radius-base)' }}
|
|
||||||
>
|
|
||||||
<PlusOutlined className="font-size-14" />
|
|
||||||
{btnText ||
|
|
||||||
intl.formatMessage({
|
|
||||||
id: 'common.button.addSelector'
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ButtonWrapper>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Wrapper;
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import AutoComplete from '@/components/seal-form/auto-complete';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface HintInputProps {
|
|
||||||
value: string;
|
|
||||||
label?: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onBlur?: (e: any) => void;
|
|
||||||
onPaste?: (e: any) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
trim?: boolean;
|
|
||||||
sourceOptions?: Global.HintOptions[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchReg = /[^=]+=[^=]*$/;
|
|
||||||
|
|
||||||
const HintInput: React.FC<HintInputProps> = (props) => {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
onPaste,
|
|
||||||
sourceOptions,
|
|
||||||
trim = true
|
|
||||||
} = props;
|
|
||||||
const cursorPosRef = React.useRef(0);
|
|
||||||
const contextBeforeCursorRef = React.useRef('');
|
|
||||||
const [options, setOptions] = React.useState<
|
|
||||||
Array<Global.BaseOption<string>>
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const generateOptions = (context: string) => {
|
|
||||||
if (!context) {
|
|
||||||
setOptions(sourceOptions || []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const match = context.match(matchReg);
|
|
||||||
if (!match) {
|
|
||||||
const list = _.filter(sourceOptions, (item: Global.HintOptions) =>
|
|
||||||
item.label.includes(context)
|
|
||||||
);
|
|
||||||
setOptions(list);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [key, value] = _.split(match[0], '=');
|
|
||||||
const data = _.find(
|
|
||||||
sourceOptions,
|
|
||||||
(item: Global.HintOptions) => item.label === key
|
|
||||||
);
|
|
||||||
if (!data) {
|
|
||||||
setOptions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = _.filter(data.opts, (item: Global.BaseOption<string>) =>
|
|
||||||
item.label.includes(value)
|
|
||||||
);
|
|
||||||
setOptions(list);
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceLastEqual = (value: string) => {
|
|
||||||
const matchStr = contextBeforeCursorRef.current.match(matchReg);
|
|
||||||
if (matchStr) {
|
|
||||||
const arr = _.split(matchStr[0], '=');
|
|
||||||
onChange(`${arr[0]}=${value}`);
|
|
||||||
} else {
|
|
||||||
onChange(value?.trim());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContextBeforeCursor = _.debounce((e: any) => {
|
|
||||||
cursorPosRef.current = e.target.selectionStart;
|
|
||||||
contextBeforeCursorRef.current = e.target.value.slice(
|
|
||||||
0,
|
|
||||||
cursorPosRef.current
|
|
||||||
);
|
|
||||||
generateOptions(contextBeforeCursorRef.current);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
const handleInput = (e: any) => {
|
|
||||||
getContextBeforeCursor(e);
|
|
||||||
onChange(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnSelect = (value: string) => {
|
|
||||||
replaceLastEqual(value);
|
|
||||||
setOptions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoComplete
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
defaultActiveFirstOption={true}
|
|
||||||
value={value}
|
|
||||||
onInput={handleInput}
|
|
||||||
onSelect={handleOnSelect}
|
|
||||||
onFocus={getContextBeforeCursor}
|
|
||||||
onBlur={onBlur}
|
|
||||||
label={label}
|
|
||||||
options={options}
|
|
||||||
trim={trim}
|
|
||||||
style={{ flex: 1, minWidth: 0 }}
|
|
||||||
onPaste={onPaste}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HintInput;
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { parseParamsString } from '@/utils';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import Wrapper from '../label-selector/wrapper';
|
|
||||||
import ListItem from './list-item';
|
|
||||||
|
|
||||||
interface ListInputProps {
|
|
||||||
required?: boolean;
|
|
||||||
dataList: string[];
|
|
||||||
label?: React.ReactNode;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
btnText?: string;
|
|
||||||
options?: Global.HintOptions[];
|
|
||||||
placeholder?: string;
|
|
||||||
labelExtra?: React.ReactNode;
|
|
||||||
trim?: boolean;
|
|
||||||
styles?: {
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
item?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
onChange: (data: string[]) => void;
|
|
||||||
onBlur?: (e: any, index: number) => void;
|
|
||||||
onDelete?: (index: number) => void;
|
|
||||||
renderItem?: (
|
|
||||||
data: any,
|
|
||||||
props: {
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onBlur?: (e: any) => void;
|
|
||||||
}
|
|
||||||
) => React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListInput: React.FC<ListInputProps> = (props) => {
|
|
||||||
const {
|
|
||||||
dataList,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
onDelete,
|
|
||||||
btnText,
|
|
||||||
options,
|
|
||||||
labelExtra,
|
|
||||||
trim = true,
|
|
||||||
styles,
|
|
||||||
required,
|
|
||||||
renderItem
|
|
||||||
} = props;
|
|
||||||
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
|
||||||
const countRef = React.useRef(0);
|
|
||||||
|
|
||||||
const updateCountRef = () => {
|
|
||||||
countRef.current = countRef.current + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnRemove = (index: number) => {
|
|
||||||
const values = _.cloneDeep(list);
|
|
||||||
values.splice(index, 1);
|
|
||||||
const valueList = _.map(values, 'value').filter((val: string) => !!val);
|
|
||||||
setList(values);
|
|
||||||
onChange(valueList);
|
|
||||||
onDelete?.(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnChange = (value: string, index: number) => {
|
|
||||||
const values = _.cloneDeep(list);
|
|
||||||
values[index].value = value;
|
|
||||||
const valueList = _.map(values, 'value').filter((val: string) => !!val);
|
|
||||||
setList(values);
|
|
||||||
onChange(valueList);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnAdd = () => {
|
|
||||||
updateCountRef();
|
|
||||||
const values = _.cloneDeep(list);
|
|
||||||
values.push({
|
|
||||||
value: '',
|
|
||||||
uid: countRef.current
|
|
||||||
});
|
|
||||||
setList(values);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnPaste = (e: any, index: number) => {
|
|
||||||
const pastedText = e.clipboardData?.getData('text');
|
|
||||||
if (!pastedText) return;
|
|
||||||
|
|
||||||
const lines = parseParamsString(pastedText);
|
|
||||||
|
|
||||||
if (lines.length <= 1) {
|
|
||||||
// if there's only one line, let the default paste behavior handle it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const values = _.cloneDeep(list);
|
|
||||||
|
|
||||||
// replace the current item with the first line of the pasted text
|
|
||||||
values[index].value = trim ? lines[0].trim() : lines[0];
|
|
||||||
|
|
||||||
// create new list items for the remaining lines
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
|
||||||
updateCountRef();
|
|
||||||
values.splice(index + i, 0, {
|
|
||||||
value: trim ? lines[i].trim() : lines[i],
|
|
||||||
uid: countRef.current
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueList = _.map(values, 'value').filter((val: string) => !!val);
|
|
||||||
setList(values);
|
|
||||||
onChange(valueList);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const valueList = _.map(list, 'value').filter((val: string) => !!val);
|
|
||||||
if (!_.isEqual(valueList, dataList)) {
|
|
||||||
const values = _.map(dataList, (value: string) => {
|
|
||||||
updateCountRef();
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
uid: countRef.current
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setList(values);
|
|
||||||
}
|
|
||||||
}, [dataList]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (required && list.length === 0) {
|
|
||||||
handleOnAdd();
|
|
||||||
}
|
|
||||||
}, [required]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
styles={styles}
|
|
||||||
label={label}
|
|
||||||
required={required}
|
|
||||||
description={description}
|
|
||||||
labelExtra={labelExtra}
|
|
||||||
onAdd={handleOnAdd}
|
|
||||||
btnText={btnText}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{_.map(list, (item: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
required={required && list.length === 1}
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
options={options}
|
|
||||||
data={item}
|
|
||||||
key={item.uid}
|
|
||||||
value={item.value}
|
|
||||||
onBlur={(e) => onBlur?.(e, index)}
|
|
||||||
onRemove={() => handleOnRemove(index)}
|
|
||||||
onChange={(val) => handleOnChange(val, index)}
|
|
||||||
onPaste={(e) => handleOnPaste(e, index)}
|
|
||||||
trim={trim}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListInput;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { MinusOutlined } from '@ant-design/icons';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import HintInput from './hint-input';
|
|
||||||
import './styles/list-item.less';
|
|
||||||
|
|
||||||
interface LabelItemProps {
|
|
||||||
onRemove: () => void;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onBlur?: (e: any) => void;
|
|
||||||
onPaste?: (e: any) => void;
|
|
||||||
renderItem?: (
|
|
||||||
data: any,
|
|
||||||
props: {
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onBlur?: (e: any) => void;
|
|
||||||
onPaste?: (e: any) => void;
|
|
||||||
}
|
|
||||||
) => React.ReactNode;
|
|
||||||
value: string;
|
|
||||||
label?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
options?: Global.HintOptions[];
|
|
||||||
trim?: boolean;
|
|
||||||
data?: any;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListItem: React.FC<LabelItemProps> = (props) => {
|
|
||||||
const {
|
|
||||||
onRemove,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
onPaste,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
trim = true,
|
|
||||||
data,
|
|
||||||
required,
|
|
||||||
renderItem
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const handleOnChange = (value: any) => {
|
|
||||||
onChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="list-item">
|
|
||||||
{renderItem ? (
|
|
||||||
renderItem(data, {
|
|
||||||
onChange: handleOnChange,
|
|
||||||
onBlur,
|
|
||||||
onPaste
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<HintInput
|
|
||||||
value={value}
|
|
||||||
onChange={handleOnChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onPaste={onPaste}
|
|
||||||
label={label}
|
|
||||||
sourceOptions={options}
|
|
||||||
trim={trim}
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!required && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
className="btn"
|
|
||||||
type="default"
|
|
||||||
shape="circle"
|
|
||||||
icon={<MinusOutlined />}
|
|
||||||
onClick={onRemove}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListItem;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.list-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.field-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.list-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.field-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
export const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
|
|
||||||
export const replaceLineRegex = /\r\n/g;
|
|
||||||
|
|
||||||
export const PageSize = 1000;
|
|
||||||
|
|
||||||
export const throttle = <T extends (...args: any[]) => void>(
|
|
||||||
func: T,
|
|
||||||
wait: number
|
|
||||||
): ((this: ThisParameterType<T>, ...args: Parameters<T>) => void) => {
|
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let previous = Date.now();
|
|
||||||
|
|
||||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
|
|
||||||
const now = Date.now();
|
|
||||||
const remaining = wait - (now - previous);
|
|
||||||
const context = this as ThisParameterType<T>;
|
|
||||||
|
|
||||||
if (remaining <= 0 || remaining > wait) {
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = null;
|
|
||||||
}
|
|
||||||
previous = now;
|
|
||||||
func.apply(context, args);
|
|
||||||
} else if (!timeout) {
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
previous = Date.now();
|
|
||||||
timeout = null;
|
|
||||||
func.apply(context, args);
|
|
||||||
}, remaining);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import _, { throttle } from 'lodash';
|
|
||||||
import React, {
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import './styles/logs-list.less';
|
|
||||||
|
|
||||||
interface LogsListProps {
|
|
||||||
dataList: any[];
|
|
||||||
height?: number;
|
|
||||||
onScroll?: (data: { isTop: boolean; isBottom: boolean }) => void;
|
|
||||||
diffHeight?: number;
|
|
||||||
showNum?: boolean;
|
|
||||||
ref?: any;
|
|
||||||
}
|
|
||||||
const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
|
|
||||||
const { dataList, height, showNum, onScroll, diffHeight = 96 } = props;
|
|
||||||
const {
|
|
||||||
initialize,
|
|
||||||
updateScrollerPosition,
|
|
||||||
updateScrollerPositionToTop,
|
|
||||||
generateInstance,
|
|
||||||
scrollEventElement,
|
|
||||||
instance,
|
|
||||||
initialized
|
|
||||||
} = useOverlayScroller({
|
|
||||||
options: {
|
|
||||||
scrollbars: {
|
|
||||||
theme: 'os-theme-light'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [innerHieght, setInnerHeight] = useState(
|
|
||||||
window.innerHeight - diffHeight
|
|
||||||
);
|
|
||||||
const scroller = useRef<any>({});
|
|
||||||
const stopScroll = useRef(false);
|
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
|
||||||
updateScrollerPosition(0);
|
|
||||||
}, [updateScrollerPosition]);
|
|
||||||
|
|
||||||
const debounceResetStopScroll = _.debounce(() => {
|
|
||||||
stopScroll.current = false;
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
const scrollToTop = useCallback(() => {
|
|
||||||
stopScroll.current = true;
|
|
||||||
updateScrollerPositionToTop();
|
|
||||||
debounceResetStopScroll();
|
|
||||||
}, [updateScrollerPositionToTop]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
scrollToBottom,
|
|
||||||
scrollToTop,
|
|
||||||
scroller: scroller.current
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleOnWheel = (e: any) => {
|
|
||||||
const scrollTop = scrollEventElement?.current?.scrollTop;
|
|
||||||
const scrollHeight = scrollEventElement?.current?.scrollHeight;
|
|
||||||
const clientHeight = scrollEventElement?.current?.clientHeight;
|
|
||||||
|
|
||||||
stopScroll.current = scrollTop + clientHeight <= scrollHeight;
|
|
||||||
|
|
||||||
const isBottom = scrollTop + clientHeight + 150 >= scrollHeight;
|
|
||||||
// is scroll to top
|
|
||||||
if (scrollTop <= 10) {
|
|
||||||
onScroll?.({
|
|
||||||
isTop: true,
|
|
||||||
isBottom: false
|
|
||||||
});
|
|
||||||
} else if (isBottom) {
|
|
||||||
onScroll?.({
|
|
||||||
isTop: false,
|
|
||||||
isBottom: true
|
|
||||||
});
|
|
||||||
stopScroll.current = false;
|
|
||||||
} else {
|
|
||||||
onScroll?.({
|
|
||||||
isTop: false,
|
|
||||||
isBottom: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debounceUpdateScrollerPosition = _.debounce(() => {
|
|
||||||
generateInstance();
|
|
||||||
updateScrollerPosition(0);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = throttle(() => {
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const viewHeight = viewportHeight - diffHeight;
|
|
||||||
setInnerHeight(viewHeight);
|
|
||||||
}, 100);
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
}, [diffHeight]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scroller.current) {
|
|
||||||
initialize(scroller.current);
|
|
||||||
}
|
|
||||||
}, [initialize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dataList.length && !stopScroll.current && instance) {
|
|
||||||
updateScrollerPosition(0);
|
|
||||||
} else if (dataList.length && !stopScroll.current && scroller.current) {
|
|
||||||
if (!initialized) {
|
|
||||||
initialize(scroller.current);
|
|
||||||
}
|
|
||||||
if (!instance) {
|
|
||||||
debounceUpdateScrollerPosition();
|
|
||||||
} else {
|
|
||||||
updateScrollerPosition(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [dataList, stopScroll.current, instance, scroller.current]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="logs-wrap"
|
|
||||||
style={{ height: innerHieght }}
|
|
||||||
ref={scroller}
|
|
||||||
onWheel={handleOnWheel}
|
|
||||||
>
|
|
||||||
<div className={classNames('content')}>
|
|
||||||
{_.map(dataList, (item: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.uid}
|
|
||||||
className={classNames('text')}
|
|
||||||
data-uid={item.uid}
|
|
||||||
>
|
|
||||||
{item.content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default React.memo(LogsList);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import {
|
|
||||||
DownOutlined,
|
|
||||||
UpOutlined,
|
|
||||||
VerticalLeftOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import './styles/pagination.less';
|
|
||||||
|
|
||||||
interface LogsPaginationProps {
|
|
||||||
page: number;
|
|
||||||
total: number;
|
|
||||||
pageSize?: number;
|
|
||||||
onPrev?: () => void;
|
|
||||||
onNext?: () => void;
|
|
||||||
onBackend?: () => void;
|
|
||||||
onToFirst?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LogsPagination: React.FC<LogsPaginationProps> = (props) => {
|
|
||||||
const { page, total, pageSize, onNext, onPrev, onBackend, onToFirst } = props;
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const handleOnPrev = () => {
|
|
||||||
onPrev?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnNext = () => {
|
|
||||||
onNext?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pagination">
|
|
||||||
{
|
|
||||||
<>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: 'models.logs.pagination.first' })}
|
|
||||||
placement="left"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={onToFirst}
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
style={{ color: 'rgba(255,255,255,.7)', marginBottom: 10 }}
|
|
||||||
>
|
|
||||||
<VerticalLeftOutlined rotate={-90} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
placement="left"
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{ id: 'models.logs.pagination.prev' },
|
|
||||||
{ lines: pageSize }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleOnPrev}
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
style={{ color: 'rgba(255,255,255,.7)' }}
|
|
||||||
>
|
|
||||||
<UpOutlined />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<span className="pages">
|
|
||||||
<span className="curr">{page}</span> /{' '}
|
|
||||||
<span className="total">{total}</span>
|
|
||||||
</span>
|
|
||||||
{page < total && (
|
|
||||||
<>
|
|
||||||
<Tooltip
|
|
||||||
placement="left"
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{ id: 'models.logs.pagination.next' },
|
|
||||||
{ lines: pageSize }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleOnNext}
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
style={{ color: 'rgba(255,255,255,.7)' }}
|
|
||||||
>
|
|
||||||
<DownOutlined />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: 'models.logs.pagination.last' })}
|
|
||||||
placement="left"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={onBackend}
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
style={{ color: 'rgba(255,255,255,.7)', marginTop: 10 }}
|
|
||||||
>
|
|
||||||
<VerticalLeftOutlined rotate={90} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogsPagination;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user