diff --git a/packages/core/client/src/built-in/admin-layout/index.tsx b/packages/core/client/src/built-in/admin-layout/index.tsx index 6bc37b4f6..53041be9a 100644 --- a/packages/core/client/src/built-in/admin-layout/index.tsx +++ b/packages/core/client/src/built-in/admin-layout/index.tsx @@ -19,6 +19,7 @@ import { SchemaComponent, useACLRoleContext, useAdminSchemaUid, + useApp, useDocumentTitle, useRequest, useSystemSettings, @@ -403,6 +404,7 @@ const SetThemeOfHeaderSubmenu = ({ children }) => { export const InternalAdminLayout = (props: any) => { const sideMenuRef = useRef(); const result = useSystemSettings(); + const app = useApp(); const params = useParams(); const { styles } = useStyles(); return ( @@ -411,7 +413,12 @@ export const InternalAdminLayout = (props: any) => {
-
+
{ + location.href = app.adminUrl; + }} + >

{result?.data?.data?.title}

diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index a6fdb51f4..6b2408183 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -48,6 +48,7 @@ export * from './modules/blocks/data-blocks/form'; export * from './modules/blocks/data-blocks/table'; export * from './modules/blocks/data-blocks/table-selector'; export * from 'react-i18next'; +export { useHotkeys } from 'react-hotkeys-hook'; export * from './modules/blocks/useParentRecordCommon'; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/Assistant.provider.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/Assistant.provider.tsx index 4fbbeb3a6..8d41c7763 100644 --- a/packages/plugins/@hera/plugin-core/src/client/features/assistant/Assistant.provider.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/Assistant.provider.tsx @@ -1,60 +1,45 @@ import React from 'react'; -import { css, useDesignable } from '@tachybase/client'; +import { useDesignable, useHotkeys } from '@tachybase/client'; -import { CalculatorOutlined, CommentOutlined, HighlightOutlined, ToolOutlined } from '@ant-design/icons'; +import { + CalculatorOutlined, + CommentOutlined, + HighlightOutlined, + SearchOutlined, + ToolOutlined, +} from '@ant-design/icons'; import { FloatButton } from 'antd'; -import { createPortal } from 'react-dom'; import { useContextMenu } from '../context-menu/useContextMenu'; -import { CalculatorWrapper } from './calculator/Calculator'; +import { useCalculator } from './calculator/CalculatorProvider'; +import { useSearchAndJump } from './search-and-jump'; export const AssistantProvider = ({ children }) => { const { designable, setDesignable } = useDesignable(); + const { visible, setVisible } = useCalculator(); + const { setOpen } = useSearchAndJump(); + + // 快捷键切换编辑状态 + useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]); + useHotkeys('Ctrl+K', () => setOpen((open) => !open), []); + useHotkeys('Cmd+K', () => setOpen((open) => !open), []); + const { contextMenuEnabled, setContextMenuEnable } = useContextMenu(); return ( <> {children} }> + } onClick={() => setOpen(true)} /> } type={designable ? 'primary' : 'default'} onClick={() => setDesignable(!designable)} /> } onClick={() => { - createPortal( - //
- //
- // {/* */} - //
, -
- dsfasdfasfasdfadsf -
, - document.body, - ); - alert('---'); + setVisible((visible) => !visible); }} /> } /> diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/AutoScalingText.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/AutoScalingText.tsx index 0aa43adbb..c78cc20c5 100644 --- a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/AutoScalingText.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/AutoScalingText.tsx @@ -5,28 +5,36 @@ export const AutoScalingText = ({ children }) => { const [scale, setScale] = useState(1); useEffect(() => { - if (!ref.current) { - return; - } + const observer = new ResizeObserver((entries) => { + const node = entries[0].target as HTMLDivElement; + const parentNode = node.parentNode as HTMLDivElement; + + const availableWidth = parentNode.offsetWidth; + const actualWidth = node.offsetWidth; + const actualScale = availableWidth / actualWidth; + + setScale((scale) => { + if (actualScale < 1) { + return actualScale; + } else if (scale < 1) { + return 1; + } + }); + }); const node = ref.current; - const parentNode = node.parentNode; - - // @ts-ignore - const availableWidth = parentNode.offsetWidth; - const actualWidth = node.offsetWidth; - const actualScale = availableWidth / actualWidth; - - if (scale === actualScale) return; - - if (actualScale < 1) { - setScale(actualScale); - } else if (scale < 1) { - setScale(1); - } - }, [scale]); + observer.observe(node); + return () => observer.unobserve(node); + }, []); return ( -
+
{children}
); diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Calculator.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Calculator.tsx index 75335e7df..9fdf8ab91 100644 --- a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Calculator.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Calculator.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { css } from '@tachybase/client'; import { CalculatorDisplay } from './CalculatorDisplay'; import { CalculatorKey } from './CalculatorKey'; @@ -12,48 +13,33 @@ const CalculatorOperations = { '=': (prevValue, nextValue) => nextValue, }; -class Calculator extends React.Component { - state = { - value: null, - displayValue: '0', - operator: null, - waitingForOperand: false, +const Calculator = React.forwardRef<{ className: string }, any>(({ className }, ref) => { + const [value, setValue] = useState(null); + const [displayValue, setDisplayValue] = useState('0'); + const [operator, setOperator] = useState(null); + const [waitingForOperand, setWaitingForOperand] = useState(false); + + const clearAll = () => { + setValue(null); + setDisplayValue('0'); + setOperator(null); + setWaitingForOperand(false); }; - clearAll() { - this.setState({ - value: null, - displayValue: '0', - operator: null, - waitingForOperand: false, - }); - } + const clearDisplay = () => { + setDisplayValue('0'); + }; - clearDisplay() { - this.setState({ - displayValue: '0', - }); - } + const clearLastChar = () => { + setDisplayValue(displayValue.substring(0, displayValue.length - 1) || '0'); + }; - clearLastChar() { - const { displayValue } = this.state; - - this.setState({ - displayValue: displayValue.substring(0, displayValue.length - 1) || '0', - }); - } - - toggleSign() { - const { displayValue } = this.state; + const toggleSign = () => { const newValue = parseFloat(displayValue) * -1; + setDisplayValue(String(newValue)); + }; - this.setState({ - displayValue: String(newValue), - }); - } - - inputPercent() { - const { displayValue } = this.state; + const inputPercent = () => { const currentValue = parseFloat(displayValue); if (currentValue === 0) return; @@ -61,187 +47,155 @@ class Calculator extends React.Component { const fixedDigits = displayValue.replace(/^-?\d*\.?/, ''); const newValue = parseFloat(displayValue) / 100; - this.setState({ - displayValue: String(newValue.toFixed(fixedDigits.length + 2)), - }); - } - - inputDot() { - const { displayValue } = this.state; + setDisplayValue(String(newValue.toFixed(fixedDigits.length + 2))); + }; + const inputDot = () => { if (!/\./.test(displayValue)) { - this.setState({ - displayValue: displayValue + '.', - waitingForOperand: false, - }); - } - } - - inputDigit(digit) { - const { displayValue, waitingForOperand } = this.state; - - if (waitingForOperand) { - this.setState({ - displayValue: String(digit), - waitingForOperand: false, - }); - } else { - this.setState({ - displayValue: displayValue === '0' ? String(digit) : displayValue + digit, - }); - } - } - - performOperation(nextOperator) { - const { value, displayValue, operator } = this.state; - const inputValue = parseFloat(displayValue); - - if (value == null) { - this.setState({ - value: inputValue, - }); - } else if (operator) { - const currentValue = value || 0; - const newValue = CalculatorOperations[operator](currentValue, inputValue); - - this.setState({ - value: newValue, - displayValue: String(newValue), - }); - } - - this.setState({ - waitingForOperand: true, - operator: nextOperator, - }); - } - - handleKeyDown = (event) => { - let { key } = event; - - if (key === 'Enter') key = '='; - - if (/\d/.test(key)) { - event.preventDefault(); - this.inputDigit(parseInt(key, 10)); - } else if (key in CalculatorOperations) { - event.preventDefault(); - this.performOperation(key); - } else if (key === '.') { - event.preventDefault(); - this.inputDot(); - } else if (key === '%') { - event.preventDefault(); - this.inputPercent(); - } else if (key === 'Backspace') { - event.preventDefault(); - this.clearLastChar(); - } else if (key === 'Clear') { - event.preventDefault(); - - if (this.state.displayValue !== '0') { - this.clearDisplay(); - } else { - this.clearAll(); - } + setDisplayValue(displayValue + '.'); + setWaitingForOperand(false); } }; - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown); - } + const inputDigit = (digit) => { + if (waitingForOperand) { + setDisplayValue(String(digit)); + setWaitingForOperand(false); + } else { + setDisplayValue(displayValue === '0' ? String(digit) : displayValue + digit); + } + }; - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyDown); - } + const performOperation = (nextOperator) => { + const inputValue = parseFloat(displayValue); - render() { - const { displayValue } = this.state; + if (value == null) { + setValue(inputValue); + } else if (operator) { + const currentValue = value || 0; + const newValue = CalculatorOperations[operator](currentValue, inputValue); + setValue(newValue); + setDisplayValue(String(newValue)); + } + setWaitingForOperand(true); + setOperator(nextOperator); + }; - const clearDisplay = displayValue !== '0'; - const clearText = clearDisplay ? 'C' : 'AC'; + useEffect(() => { + const handleKeyDown = (event) => { + let { key } = event; - return ( -
-
- -
-
-
- (clearDisplay ? this.clearDisplay() : this.clearAll())} - > - {clearText} - - this.toggleSign()}> - ± - - this.inputPercent()}> - % - -
-
- this.inputDigit(0)}> - 0 - - this.inputDot()}> - ● - - this.inputDigit(1)}> - 1 - - this.inputDigit(2)}> - 2 - - this.inputDigit(3)}> - 3 - - this.inputDigit(4)}> - 4 - - this.inputDigit(5)}> - 5 - - this.inputDigit(6)}> - 6 - - this.inputDigit(7)}> - 7 - - this.inputDigit(8)}> - 8 - - this.inputDigit(9)}> - 9 - -
-
-
- this.performOperation('/')}> - ÷ - - this.performOperation('*')}> - × - - this.performOperation('-')}> - − - - this.performOperation('+')}> - + - - this.performOperation('=')}> - = - -
+ if (key === 'Enter') key = '='; + + if (/\d/.test(key)) { + event.preventDefault(); + inputDigit(parseInt(key, 10)); + } else if (key in CalculatorOperations) { + event.preventDefault(); + performOperation(key); + } else if (key === '.') { + event.preventDefault(); + inputDot(); + } else if (key === '%') { + event.preventDefault(); + inputPercent(); + } else if (key === 'Backspace') { + event.preventDefault(); + clearLastChar(); + } else if (key === 'Clear') { + event.preventDefault(); + + if (displayValue !== '0') { + clearDisplay(); + } else { + clearAll(); + } + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }); + + const clearKey = displayValue !== '0'; + const clearText = clearKey ? 'C' : 'AC'; + + return ( +
+ +
+
+
+ (clearKey ? clearDisplay() : clearAll())}> + {clearText} + + toggleSign()}> + ± + + inputPercent()}> + % + +
+
+ inputDigit(0)}> + 0 + + inputDot()}> + ● + + inputDigit(1)}> + 1 + + inputDigit(2)}> + 2 + + inputDigit(3)}> + 3 + + inputDigit(4)}> + 4 + + inputDigit(5)}> + 5 + + inputDigit(6)}> + 6 + + inputDigit(7)}> + 7 + + inputDigit(8)}> + 8 + + inputDigit(9)}> + 9 +
+
+ performOperation('/')}> + ÷ + + performOperation('*')}> + × + + performOperation('-')}> + − + + performOperation('+')}> + + + + performOperation('=')}> + = + +
- ); - } -} +
+ ); +}); -export const CalculatorWrapper = () => { +export const CalculatorWrapper = React.forwardRef((props, ref) => { const { styles } = useStyles(); - return ; -}; + return ; +}); diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/CalculatorProvider.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/CalculatorProvider.tsx new file mode 100644 index 000000000..730c55cbb --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/CalculatorProvider.tsx @@ -0,0 +1,38 @@ +import React, { createContext, useContext, useState } from 'react'; + +import { createPortal } from 'react-dom'; + +import { CalculatorWrapper } from './Calculator'; +import { Draggable } from './Draggable'; +import { useStyles } from './style'; + +export interface CalculatorProps { + visible: any; + setVisible: any; +} + +export const CalculatorContext = createContext>({}); +export const useCalculator = () => { + return useContext(CalculatorContext); +}; + +export const CalculatorProvider = ({ children }) => { + const [visible, setVisible] = useState(false); + const { styles } = useStyles(); + return ( + + {children} + + {visible + ? createPortal( +
+ + + +
, + document.body, + ) + : null} +
+ ); +}; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Draggable.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Draggable.tsx new file mode 100644 index 000000000..0425af5b2 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/Draggable.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useRef } from 'react'; + +export const Draggable = ({ + children, + updateTransform = (transformStr, tx, ty, tdom) => { + tdom.style.transform = transformStr; + }, +}) => { + const ref = useRef(); + const positionRef = useRef({ + startX: 0, + startY: 0, + dx: 0, + dy: 0, + tx: 0, + ty: 0, + }); + + useEffect(() => { + const node = ref.current; + if (!node) { + return; + } + const start = (event) => { + if (event.button !== 0) { + //只允许左键,右键问题在于不选择conextmenu就不会触发mouseup事件 + return; + } + document.addEventListener('mousemove', docMove); + positionRef.current.startX = event.pageX - positionRef.current.dx; + positionRef.current.startY = event.pageY - positionRef.current.dy; + }; + const docMove = (event) => { + const tx = event.pageX - positionRef.current.startX; + const ty = event.pageY - positionRef.current.startY; + const transformStr = `translate(${tx}px,${ty}px)`; + updateTransform(transformStr, tx, ty, node); + positionRef.current.dx = tx; + positionRef.current.dy = ty; + }; + const docMouseUp = (event) => { + document.removeEventListener('mousemove', docMove); + }; + + node.addEventListener('mousedown', start); + //用document移除对mousemove事件的监听 + document.addEventListener('mouseup', docMouseUp); + return () => { + node.removeEventListener('mousedown', start); + document.removeEventListener('mouseup', docMouseUp); + }; + }, [updateTransform]); + + const newStyle = { ...children.props.style, cursor: 'move', userSelect: 'none' }; + return React.cloneElement(React.Children.only(children), { + ref, + style: newStyle, + }); +}; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/style.ts b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/style.ts index 1df0d34fa..0f44f9cee 100644 --- a/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/style.ts +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/calculator/style.ts @@ -3,6 +3,14 @@ import { createStyles } from '@tachybase/client'; export const useStyles = createStyles(({ css }) => { return { container: css` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 320px; + height: 520px; + z-index: 1350; + button { display: block; background: none; @@ -20,20 +28,6 @@ export const useStyles = createStyles(({ css }) => { box-shadow: inset 0px 0px 80px 0px rgba(0, 0, 0, 0.25); } - #wrapper { - height: 100vh; - - display: flex; - align-items: center; - justify-content: center; - } - - #app { - width: 320px; - height: 520px; - position: relative; - } - .calculator { width: 100%; height: 100%; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/index.ts b/packages/plugins/@hera/plugin-core/src/client/features/assistant/index.ts index c35d1a9a2..bdba41140 100644 --- a/packages/plugins/@hera/plugin-core/src/client/features/assistant/index.ts +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/index.ts @@ -1,9 +1,13 @@ import { Plugin } from '@tachybase/client'; import { AssistantProvider } from './Assistant.provider'; +import { CalculatorProvider } from './calculator/CalculatorProvider'; +import { SearchAndJumpProvider } from './search-and-jump'; export class PluginAssistant extends Plugin { async load() { + this.app.use(SearchAndJumpProvider); + this.app.use(CalculatorProvider); this.app.use(AssistantProvider); } } diff --git a/packages/plugins/@hera/plugin-core/src/client/features/assistant/search-and-jump/index.tsx b/packages/plugins/@hera/plugin-core/src/client/features/assistant/search-and-jump/index.tsx new file mode 100644 index 000000000..91602c557 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/assistant/search-and-jump/index.tsx @@ -0,0 +1,55 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; + +import { Input, InputRef, Modal } from 'antd'; + +export const SearchAndJumpContext = createContext({ + open: false, + setOpen: (open: boolean | ((open: boolean) => boolean)) => {}, +}); + +export const SearchAndJumpProvider = ({ children }) => { + const [open, setOpen] = useState(false); + + return ( + + {children} + + + ); +}; + +export const useSearchAndJump = () => { + return useContext(SearchAndJumpContext); +}; + +export const SearchAndJump = ({ open, setOpen }) => { + const inputRef = useRef(); + useEffect(() => { + if (open && inputRef.current) { + setTimeout(() => { + inputRef.current.focus(); + }, 0); + } + }, [open, inputRef]); + + const handleOk = () => { + setOpen(false); + }; + + const handleCancel = () => { + setOpen(false); + }; + + return ( + } + closable={false} + open={open} + onOk={handleOk} + onCancel={handleCancel} + footer={null} + > +

+
+ ); +};