mirror of https://github.com/fantasticit/think.git
feat: add zoom handler
parent
53c37ae052
commit
e7259db747
|
@ -30,4 +30,16 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handlerWrap {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: var(--semi-color-bg-2);
|
||||||
|
border: 1px solid var(--node-border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@ import React from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
|
|
||||||
import { Space, Spin, Typography } from '@douyinfe/semi-ui';
|
import { Button, Space, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
import { NodeViewWrapper } from '@tiptap/react';
|
import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||||
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant';
|
||||||
|
import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { IconMind } from 'components/icons';
|
import { IconMind, IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
|
||||||
|
@ -30,6 +32,7 @@ export const _ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const [loading, toggleLoading] = useToggle(true);
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
const [zoom, setZoomState] = useState(100);
|
||||||
|
|
||||||
const onResize = useCallback(
|
const onResize = useCallback(
|
||||||
(size) => {
|
(size) => {
|
||||||
|
@ -47,6 +50,14 @@ export const _ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
[toggleVisible]
|
[toggleVisible]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setZoom = useCallback((type: 'minus' | 'plus') => {
|
||||||
|
return () => {
|
||||||
|
setZoomState((currentZoom) =>
|
||||||
|
clamp(type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isUnmount = false;
|
let isUnmount = false;
|
||||||
|
|
||||||
|
@ -114,6 +125,8 @@ export const _ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
transform: `scale(${zoom / 100})`,
|
||||||
|
transition: `all ease-in-out .3s`,
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: Svg?.outerHTML ?? '' }}
|
dangerouslySetInnerHTML={{ __html: Svg?.outerHTML ?? '' }}
|
||||||
/>
|
/>
|
||||||
|
@ -127,6 +140,27 @@ export const _ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
绘图
|
绘图
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.handlerWrap}>
|
||||||
|
<Tooltip content="缩小">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomOut />}
|
||||||
|
onClick={setZoom('minus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="放大">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomIn />}
|
||||||
|
onClick={setZoom('plus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Resizeable>
|
</Resizeable>
|
||||||
</VisibilitySensor>
|
</VisibilitySensor>
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
.wrap {
|
.wrap {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid var(--node-border-color);
|
border: 1px solid var(--node-border-color);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.handlerWrap {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: var(--semi-color-bg-2);
|
||||||
|
border: 1px solid var(--node-border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||||
|
|
||||||
import { Spin, Typography } from '@douyinfe/semi-ui';
|
import { Button, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
import { NodeViewWrapper } from '@tiptap/react';
|
import { NodeViewWrapper } from '@tiptap/react';
|
||||||
|
import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant';
|
||||||
import {
|
import {
|
||||||
|
clamp,
|
||||||
extractFileExtension,
|
extractFileExtension,
|
||||||
extractFilename,
|
extractFilename,
|
||||||
getEditorContainerDOMSize,
|
getEditorContainerDOMSize,
|
||||||
getImageWidthHeight,
|
getImageWidthHeight,
|
||||||
} from 'tiptap/prose-utils';
|
} from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
import { IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { uploadFile } from 'services/file';
|
import { uploadFile } from 'services/file';
|
||||||
|
|
||||||
|
@ -25,6 +29,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
const $upload = useRef<HTMLInputElement>();
|
const $upload = useRef<HTMLInputElement>();
|
||||||
const [loading, toggleLoading] = useToggle(false);
|
const [loading, toggleLoading] = useToggle(false);
|
||||||
|
const [zoom, setZoomState] = useState(100);
|
||||||
|
|
||||||
const onResize = useCallback(
|
const onResize = useCallback(
|
||||||
(size) => {
|
(size) => {
|
||||||
|
@ -64,6 +69,14 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
[updateAttributes, toggleLoading]
|
[updateAttributes, toggleLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setZoom = useCallback((type: 'minus' | 'plus') => {
|
||||||
|
return () => {
|
||||||
|
setZoomState((currentZoom) =>
|
||||||
|
clamp(type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!src && !hasTrigger) {
|
if (!src && !hasTrigger) {
|
||||||
selectFile();
|
selectFile();
|
||||||
|
@ -93,7 +106,44 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LazyLoadImage src={src} alt={alt} width={'100%'} height={'100%'} />
|
<div className={styles.wrap}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
padding: 24,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
transform: `scale(${zoom / 100})`,
|
||||||
|
transition: `all ease-in-out .3s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LazyLoadImage src={src} alt={alt} width={'100%'} height={'100%'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.handlerWrap}>
|
||||||
|
<Tooltip content="缩小">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomOut />}
|
||||||
|
onClick={setZoom('minus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="放大">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomIn />}
|
||||||
|
onClick={setZoom('plus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Resizeable>
|
</Resizeable>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class UpdateWikiDto {
|
||||||
|
|
||||||
@IsString({ message: '知识库描述类型错误(正确类型为:String)' })
|
@IsString({ message: '知识库描述类型错误(正确类型为:String)' })
|
||||||
@IsNotEmpty({ message: '知识库描述不能为空' })
|
@IsNotEmpty({ message: '知识库描述不能为空' })
|
||||||
@MinLength(3, { message: '知识库描述至少3个字符' })
|
@MinLength(1, { message: '知识库描述至少1个字符' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue