feat: searchJump and calculator

Reviewed-on: daoyoucloud/tachybase#1246
This commit is contained in:
sealday 2024-07-01 20:48:37 +08:00
parent c484545c39
commit 6af6e9a1f8
10 changed files with 382 additions and 277 deletions

View File

@ -19,6 +19,7 @@ import {
SchemaComponent, SchemaComponent,
useACLRoleContext, useACLRoleContext,
useAdminSchemaUid, useAdminSchemaUid,
useApp,
useDocumentTitle, useDocumentTitle,
useRequest, useRequest,
useSystemSettings, useSystemSettings,
@ -403,6 +404,7 @@ const SetThemeOfHeaderSubmenu = ({ children }) => {
export const InternalAdminLayout = (props: any) => { export const InternalAdminLayout = (props: any) => {
const sideMenuRef = useRef<HTMLDivElement>(); const sideMenuRef = useRef<HTMLDivElement>();
const result = useSystemSettings(); const result = useSystemSettings();
const app = useApp();
const params = useParams<any>(); const params = useParams<any>();
const { styles } = useStyles(); const { styles } = useStyles();
return ( return (
@ -411,7 +413,12 @@ export const InternalAdminLayout = (props: any) => {
<Layout.Header className={styles.header}> <Layout.Header className={styles.header}>
<div className={styles.headerA}> <div className={styles.headerA}>
<div className={styles.headerB}> <div className={styles.headerB}>
<div className={styles.titleContainer}> <div
className={styles.titleContainer}
onClick={() => {
location.href = app.adminUrl;
}}
>
<img className={styles.logo} src={result?.data?.data?.logo?.url} /> <img className={styles.logo} src={result?.data?.data?.logo?.url} />
<h1 className={styles.title}>{result?.data?.data?.title}</h1> <h1 className={styles.title}>{result?.data?.data?.title}</h1>
</div> </div>

View File

@ -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';
export * from './modules/blocks/data-blocks/table-selector'; export * from './modules/blocks/data-blocks/table-selector';
export * from 'react-i18next'; export * from 'react-i18next';
export { useHotkeys } from 'react-hotkeys-hook';
export * from './modules/blocks/useParentRecordCommon'; export * from './modules/blocks/useParentRecordCommon';

View File

@ -1,60 +1,45 @@
import React from 'react'; 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 { FloatButton } from 'antd';
import { createPortal } from 'react-dom';
import { useContextMenu } from '../context-menu/useContextMenu'; 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 }) => { export const AssistantProvider = ({ children }) => {
const { designable, setDesignable } = useDesignable(); 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(); const { contextMenuEnabled, setContextMenuEnable } = useContextMenu();
return ( return (
<> <>
{children} {children}
<FloatButton.Group trigger="hover" type="default" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}> <FloatButton.Group trigger="hover" type="default" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}>
<FloatButton icon={<SearchOutlined />} onClick={() => setOpen(true)} />
<FloatButton <FloatButton
icon={<HighlightOutlined />} icon={<HighlightOutlined />}
type={designable ? 'primary' : 'default'} type={designable ? 'primary' : 'default'}
onClick={() => setDesignable(!designable)} onClick={() => setDesignable(!designable)}
/> />
<FloatButton <FloatButton
type={visible ? 'primary' : 'default'}
icon={<CalculatorOutlined />} icon={<CalculatorOutlined />}
onClick={() => { onClick={() => {
createPortal( setVisible((visible) => !visible);
// <div
// className={css`
// position: fixed;
// top: 0;
// left: 0;
// width: 1000px;
// height: 1000px;
// z-index: 9999;
// background-color: red;
// `}
// >
// <div />
// {/* <CalculatorWrapper /> */}
// </div>,
<div
className={css`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9000;
display: 'flex';
flex-direction: column;
`}
>
dsfasdfasfasdfadsf
</div>,
document.body,
);
alert('---');
}} }}
/> />
<FloatButton icon={<CommentOutlined />} /> <FloatButton icon={<CommentOutlined />} />

View File

@ -5,28 +5,36 @@ export const AutoScalingText = ({ children }) => {
const [scale, setScale] = useState<number>(1); const [scale, setScale] = useState<number>(1);
useEffect(() => { useEffect(() => {
if (!ref.current) { const observer = new ResizeObserver((entries) => {
return; 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 node = ref.current;
const parentNode = node.parentNode; observer.observe(node);
return () => observer.unobserve(node);
// @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]);
return ( return (
<div className="auto-scaling-text" style={{ transform: `scale(${scale},${scale})` }} ref={ref}> <div
className="auto-scaling-text"
style={{
transition: 'transform 0.5s ease',
transform: `scale(${scale},${scale})`,
}}
ref={ref}
>
{children} {children}
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { css } from '@tachybase/client';
import { CalculatorDisplay } from './CalculatorDisplay'; import { CalculatorDisplay } from './CalculatorDisplay';
import { CalculatorKey } from './CalculatorKey'; import { CalculatorKey } from './CalculatorKey';
@ -12,48 +13,33 @@ const CalculatorOperations = {
'=': (prevValue, nextValue) => nextValue, '=': (prevValue, nextValue) => nextValue,
}; };
class Calculator extends React.Component { const Calculator = React.forwardRef<{ className: string }, any>(({ className }, ref) => {
state = { const [value, setValue] = useState(null);
value: null, const [displayValue, setDisplayValue] = useState('0');
displayValue: '0', const [operator, setOperator] = useState(null);
operator: null, const [waitingForOperand, setWaitingForOperand] = useState(false);
waitingForOperand: false,
const clearAll = () => {
setValue(null);
setDisplayValue('0');
setOperator(null);
setWaitingForOperand(false);
}; };
clearAll() { const clearDisplay = () => {
this.setState({ setDisplayValue('0');
value: null, };
displayValue: '0',
operator: null,
waitingForOperand: false,
});
}
clearDisplay() { const clearLastChar = () => {
this.setState({ setDisplayValue(displayValue.substring(0, displayValue.length - 1) || '0');
displayValue: '0', };
});
}
clearLastChar() { const toggleSign = () => {
const { displayValue } = this.state;
this.setState({
displayValue: displayValue.substring(0, displayValue.length - 1) || '0',
});
}
toggleSign() {
const { displayValue } = this.state;
const newValue = parseFloat(displayValue) * -1; const newValue = parseFloat(displayValue) * -1;
setDisplayValue(String(newValue));
};
this.setState({ const inputPercent = () => {
displayValue: String(newValue),
});
}
inputPercent() {
const { displayValue } = this.state;
const currentValue = parseFloat(displayValue); const currentValue = parseFloat(displayValue);
if (currentValue === 0) return; if (currentValue === 0) return;
@ -61,187 +47,155 @@ class Calculator extends React.Component {
const fixedDigits = displayValue.replace(/^-?\d*\.?/, ''); const fixedDigits = displayValue.replace(/^-?\d*\.?/, '');
const newValue = parseFloat(displayValue) / 100; const newValue = parseFloat(displayValue) / 100;
this.setState({ setDisplayValue(String(newValue.toFixed(fixedDigits.length + 2)));
displayValue: String(newValue.toFixed(fixedDigits.length + 2)), };
});
}
inputDot() {
const { displayValue } = this.state;
const inputDot = () => {
if (!/\./.test(displayValue)) { if (!/\./.test(displayValue)) {
this.setState({ setDisplayValue(displayValue + '.');
displayValue: displayValue + '.', setWaitingForOperand(false);
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();
}
} }
}; };
componentDidMount() { const inputDigit = (digit) => {
document.addEventListener('keydown', this.handleKeyDown); if (waitingForOperand) {
} setDisplayValue(String(digit));
setWaitingForOperand(false);
} else {
setDisplayValue(displayValue === '0' ? String(digit) : displayValue + digit);
}
};
componentWillUnmount() { const performOperation = (nextOperator) => {
document.removeEventListener('keydown', this.handleKeyDown); const inputValue = parseFloat(displayValue);
}
render() { if (value == null) {
const { displayValue } = this.state; 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'; useEffect(() => {
const clearText = clearDisplay ? 'C' : 'AC'; const handleKeyDown = (event) => {
let { key } = event;
return ( if (key === 'Enter') key = '=';
<div className={this.props.className}>
<div className="calculator"> if (/\d/.test(key)) {
<CalculatorDisplay value={displayValue} /> event.preventDefault();
<div className="calculator-keypad"> inputDigit(parseInt(key, 10));
<div className="input-keys"> } else if (key in CalculatorOperations) {
<div className="function-keys"> event.preventDefault();
<CalculatorKey performOperation(key);
className="key-clear" } else if (key === '.') {
onPress={() => (clearDisplay ? this.clearDisplay() : this.clearAll())} event.preventDefault();
> inputDot();
{clearText} } else if (key === '%') {
</CalculatorKey> event.preventDefault();
<CalculatorKey className="key-sign" onPress={() => this.toggleSign()}> inputPercent();
± } else if (key === 'Backspace') {
</CalculatorKey> event.preventDefault();
<CalculatorKey className="key-percent" onPress={() => this.inputPercent()}> clearLastChar();
% } else if (key === 'Clear') {
</CalculatorKey> event.preventDefault();
</div>
<div className="digit-keys"> if (displayValue !== '0') {
<CalculatorKey className="key-0" onPress={() => this.inputDigit(0)}> clearDisplay();
0 } else {
</CalculatorKey> clearAll();
<CalculatorKey className="key-dot" onPress={() => this.inputDot()}> }
}
</CalculatorKey> };
<CalculatorKey className="key-1" onPress={() => this.inputDigit(1)}> document.addEventListener('keydown', handleKeyDown);
1 return () => {
</CalculatorKey> document.removeEventListener('keydown', handleKeyDown);
<CalculatorKey className="key-2" onPress={() => this.inputDigit(2)}> };
2 });
</CalculatorKey>
<CalculatorKey className="key-3" onPress={() => this.inputDigit(3)}> const clearKey = displayValue !== '0';
3 const clearText = clearKey ? 'C' : 'AC';
</CalculatorKey>
<CalculatorKey className="key-4" onPress={() => this.inputDigit(4)}> return (
4 <div className="calculator" ref={ref as any}>
</CalculatorKey> <CalculatorDisplay value={displayValue} />
<CalculatorKey className="key-5" onPress={() => this.inputDigit(5)}> <div className="calculator-keypad">
5 <div className="input-keys">
</CalculatorKey> <div className="function-keys">
<CalculatorKey className="key-6" onPress={() => this.inputDigit(6)}> <CalculatorKey className="key-clear" onPress={() => (clearKey ? clearDisplay() : clearAll())}>
6 {clearText}
</CalculatorKey> </CalculatorKey>
<CalculatorKey className="key-7" onPress={() => this.inputDigit(7)}> <CalculatorKey className="key-sign" onPress={() => toggleSign()}>
7 ±
</CalculatorKey> </CalculatorKey>
<CalculatorKey className="key-8" onPress={() => this.inputDigit(8)}> <CalculatorKey className="key-percent" onPress={() => inputPercent()}>
8 %
</CalculatorKey> </CalculatorKey>
<CalculatorKey className="key-9" onPress={() => this.inputDigit(9)}> </div>
9 <div className="digit-keys">
</CalculatorKey> <CalculatorKey className="key-0" onPress={() => inputDigit(0)}>
</div> 0
</div> </CalculatorKey>
<div className="operator-keys"> <CalculatorKey className="key-dot" onPress={() => inputDot()}>
<CalculatorKey className="key-divide" onPress={() => this.performOperation('/')}>
÷ </CalculatorKey>
</CalculatorKey> <CalculatorKey className="key-1" onPress={() => inputDigit(1)}>
<CalculatorKey className="key-multiply" onPress={() => this.performOperation('*')}> 1
× </CalculatorKey>
</CalculatorKey> <CalculatorKey className="key-2" onPress={() => inputDigit(2)}>
<CalculatorKey className="key-subtract" onPress={() => this.performOperation('-')}> 2
</CalculatorKey>
</CalculatorKey> <CalculatorKey className="key-3" onPress={() => inputDigit(3)}>
<CalculatorKey className="key-add" onPress={() => this.performOperation('+')}> 3
+ </CalculatorKey>
</CalculatorKey> <CalculatorKey className="key-4" onPress={() => inputDigit(4)}>
<CalculatorKey className="key-equals" onPress={() => this.performOperation('=')}> 4
= </CalculatorKey>
</CalculatorKey> <CalculatorKey className="key-5" onPress={() => inputDigit(5)}>
</div> 5
</CalculatorKey>
<CalculatorKey className="key-6" onPress={() => inputDigit(6)}>
6
</CalculatorKey>
<CalculatorKey className="key-7" onPress={() => inputDigit(7)}>
7
</CalculatorKey>
<CalculatorKey className="key-8" onPress={() => inputDigit(8)}>
8
</CalculatorKey>
<CalculatorKey className="key-9" onPress={() => inputDigit(9)}>
9
</CalculatorKey>
</div> </div>
</div> </div>
<div className="operator-keys">
<CalculatorKey className="key-divide" onPress={() => performOperation('/')}>
÷
</CalculatorKey>
<CalculatorKey className="key-multiply" onPress={() => performOperation('*')}>
×
</CalculatorKey>
<CalculatorKey className="key-subtract" onPress={() => performOperation('-')}>
</CalculatorKey>
<CalculatorKey className="key-add" onPress={() => performOperation('+')}>
+
</CalculatorKey>
<CalculatorKey className="key-equals" onPress={() => performOperation('=')}>
=
</CalculatorKey>
</div>
</div> </div>
); </div>
} );
} });
export const CalculatorWrapper = () => { export const CalculatorWrapper = React.forwardRef((props, ref) => {
const { styles } = useStyles(); const { styles } = useStyles();
return <Calculator className={styles.container} />; return <Calculator ref={ref as any} className={styles.container} />;
}; });

View File

@ -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<Partial<CalculatorProps>>({});
export const useCalculator = () => {
return useContext(CalculatorContext);
};
export const CalculatorProvider = ({ children }) => {
const [visible, setVisible] = useState<boolean>(false);
const { styles } = useStyles();
return (
<CalculatorContext.Provider value={{ visible, setVisible }}>
{children}
{visible
? createPortal(
<div className={styles.container}>
<Draggable>
<CalculatorWrapper />
</Draggable>
</div>,
document.body,
)
: null}
</CalculatorContext.Provider>
);
};

View File

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

View File

@ -3,6 +3,14 @@ import { createStyles } from '@tachybase/client';
export const useStyles = createStyles(({ css }) => { export const useStyles = createStyles(({ css }) => {
return { return {
container: css` container: css`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 320px;
height: 520px;
z-index: 1350;
button { button {
display: block; display: block;
background: none; background: none;
@ -20,20 +28,6 @@ export const useStyles = createStyles(({ css }) => {
box-shadow: inset 0px 0px 80px 0px rgba(0, 0, 0, 0.25); 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 { .calculator {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -1,9 +1,13 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { AssistantProvider } from './Assistant.provider'; import { AssistantProvider } from './Assistant.provider';
import { CalculatorProvider } from './calculator/CalculatorProvider';
import { SearchAndJumpProvider } from './search-and-jump';
export class PluginAssistant extends Plugin { export class PluginAssistant extends Plugin {
async load() { async load() {
this.app.use(SearchAndJumpProvider);
this.app.use(CalculatorProvider);
this.app.use(AssistantProvider); this.app.use(AssistantProvider);
} }
} }

View File

@ -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<boolean>(false);
return (
<SearchAndJumpContext.Provider value={{ open, setOpen }}>
{children}
<SearchAndJump open={open} setOpen={setOpen} />
</SearchAndJumpContext.Provider>
);
};
export const useSearchAndJump = () => {
return useContext(SearchAndJumpContext);
};
export const SearchAndJump = ({ open, setOpen }) => {
const inputRef = useRef<InputRef>();
useEffect(() => {
if (open && inputRef.current) {
setTimeout(() => {
inputRef.current.focus();
}, 0);
}
}, [open, inputRef]);
const handleOk = () => {
setOpen(false);
};
const handleCancel = () => {
setOpen(false);
};
return (
<Modal
title={<Input style={{ width: '100%' }} autoFocus ref={inputRef} />}
closable={false}
open={open}
onOk={handleOk}
onCancel={handleCancel}
footer={null}
>
<p></p>
</Modal>
);
};