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,
useACLRoleContext,
useAdminSchemaUid,
useApp,
useDocumentTitle,
useRequest,
useSystemSettings,
@ -403,6 +404,7 @@ const SetThemeOfHeaderSubmenu = ({ children }) => {
export const InternalAdminLayout = (props: any) => {
const sideMenuRef = useRef<HTMLDivElement>();
const result = useSystemSettings();
const app = useApp();
const params = useParams<any>();
const { styles } = useStyles();
return (
@ -411,7 +413,12 @@ export const InternalAdminLayout = (props: any) => {
<Layout.Header className={styles.header}>
<div className={styles.headerA}>
<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} />
<h1 className={styles.title}>{result?.data?.data?.title}</h1>
</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-selector';
export * from 'react-i18next';
export { useHotkeys } from 'react-hotkeys-hook';
export * from './modules/blocks/useParentRecordCommon';

View File

@ -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}
<FloatButton.Group trigger="hover" type="default" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}>
<FloatButton icon={<SearchOutlined />} onClick={() => setOpen(true)} />
<FloatButton
icon={<HighlightOutlined />}
type={designable ? 'primary' : 'default'}
onClick={() => setDesignable(!designable)}
/>
<FloatButton
type={visible ? 'primary' : 'default'}
icon={<CalculatorOutlined />}
onClick={() => {
createPortal(
// <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('---');
setVisible((visible) => !visible);
}}
/>
<FloatButton icon={<CommentOutlined />} />

View File

@ -5,28 +5,36 @@ export const AutoScalingText = ({ children }) => {
const [scale, setScale] = useState<number>(1);
useEffect(() => {
if (!ref.current) {
return;
}
const node = ref.current;
const parentNode = node.parentNode;
const observer = new ResizeObserver((entries) => {
const node = entries[0].target as HTMLDivElement;
const parentNode = node.parentNode as HTMLDivElement;
// @ts-ignore
const availableWidth = parentNode.offsetWidth;
const actualWidth = node.offsetWidth;
const actualScale = availableWidth / actualWidth;
if (scale === actualScale) return;
setScale((scale) => {
if (actualScale < 1) {
setScale(actualScale);
return actualScale;
} else if (scale < 1) {
setScale(1);
return 1;
}
}, [scale]);
});
});
const node = ref.current;
observer.observe(node);
return () => observer.unobserve(node);
}, []);
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}
</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 { 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,
});
}
setDisplayValue(displayValue + '.');
setWaitingForOperand(false);
}
};
inputDigit(digit) {
const { displayValue, waitingForOperand } = this.state;
const inputDigit = (digit) => {
if (waitingForOperand) {
this.setState({
displayValue: String(digit),
waitingForOperand: false,
});
setDisplayValue(String(digit));
setWaitingForOperand(false);
} else {
this.setState({
displayValue: displayValue === '0' ? String(digit) : displayValue + digit,
});
}
setDisplayValue(displayValue === '0' ? String(digit) : displayValue + digit);
}
};
performOperation(nextOperator) {
const { value, displayValue, operator } = this.state;
const performOperation = (nextOperator) => {
const inputValue = parseFloat(displayValue);
if (value == null) {
this.setState({
value: inputValue,
});
setValue(inputValue);
} else if (operator) {
const currentValue = value || 0;
const newValue = CalculatorOperations[operator](currentValue, inputValue);
this.setState({
value: newValue,
displayValue: String(newValue),
});
setValue(newValue);
setDisplayValue(String(newValue));
}
setWaitingForOperand(true);
setOperator(nextOperator);
};
this.setState({
waitingForOperand: true,
operator: nextOperator,
});
}
handleKeyDown = (event) => {
useEffect(() => {
const handleKeyDown = (event) => {
let { key } = event;
if (key === 'Enter') key = '=';
if (/\d/.test(key)) {
event.preventDefault();
this.inputDigit(parseInt(key, 10));
inputDigit(parseInt(key, 10));
} else if (key in CalculatorOperations) {
event.preventDefault();
this.performOperation(key);
performOperation(key);
} else if (key === '.') {
event.preventDefault();
this.inputDot();
inputDot();
} else if (key === '%') {
event.preventDefault();
this.inputPercent();
inputPercent();
} else if (key === 'Backspace') {
event.preventDefault();
this.clearLastChar();
clearLastChar();
} else if (key === 'Clear') {
event.preventDefault();
if (this.state.displayValue !== '0') {
this.clearDisplay();
if (displayValue !== '0') {
clearDisplay();
} else {
this.clearAll();
clearAll();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
});
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
render() {
const { displayValue } = this.state;
const clearDisplay = displayValue !== '0';
const clearText = clearDisplay ? 'C' : 'AC';
const clearKey = displayValue !== '0';
const clearText = clearKey ? 'C' : 'AC';
return (
<div className={this.props.className}>
<div className="calculator">
<div className="calculator" ref={ref as any}>
<CalculatorDisplay value={displayValue} />
<div className="calculator-keypad">
<div className="input-keys">
<div className="function-keys">
<CalculatorKey
className="key-clear"
onPress={() => (clearDisplay ? this.clearDisplay() : this.clearAll())}
>
<CalculatorKey className="key-clear" onPress={() => (clearKey ? clearDisplay() : clearAll())}>
{clearText}
</CalculatorKey>
<CalculatorKey className="key-sign" onPress={() => this.toggleSign()}>
<CalculatorKey className="key-sign" onPress={() => toggleSign()}>
±
</CalculatorKey>
<CalculatorKey className="key-percent" onPress={() => this.inputPercent()}>
<CalculatorKey className="key-percent" onPress={() => inputPercent()}>
%
</CalculatorKey>
</div>
<div className="digit-keys">
<CalculatorKey className="key-0" onPress={() => this.inputDigit(0)}>
<CalculatorKey className="key-0" onPress={() => inputDigit(0)}>
0
</CalculatorKey>
<CalculatorKey className="key-dot" onPress={() => this.inputDot()}>
<CalculatorKey className="key-dot" onPress={() => inputDot()}>
</CalculatorKey>
<CalculatorKey className="key-1" onPress={() => this.inputDigit(1)}>
<CalculatorKey className="key-1" onPress={() => inputDigit(1)}>
1
</CalculatorKey>
<CalculatorKey className="key-2" onPress={() => this.inputDigit(2)}>
<CalculatorKey className="key-2" onPress={() => inputDigit(2)}>
2
</CalculatorKey>
<CalculatorKey className="key-3" onPress={() => this.inputDigit(3)}>
<CalculatorKey className="key-3" onPress={() => inputDigit(3)}>
3
</CalculatorKey>
<CalculatorKey className="key-4" onPress={() => this.inputDigit(4)}>
<CalculatorKey className="key-4" onPress={() => inputDigit(4)}>
4
</CalculatorKey>
<CalculatorKey className="key-5" onPress={() => this.inputDigit(5)}>
<CalculatorKey className="key-5" onPress={() => inputDigit(5)}>
5
</CalculatorKey>
<CalculatorKey className="key-6" onPress={() => this.inputDigit(6)}>
<CalculatorKey className="key-6" onPress={() => inputDigit(6)}>
6
</CalculatorKey>
<CalculatorKey className="key-7" onPress={() => this.inputDigit(7)}>
<CalculatorKey className="key-7" onPress={() => inputDigit(7)}>
7
</CalculatorKey>
<CalculatorKey className="key-8" onPress={() => this.inputDigit(8)}>
<CalculatorKey className="key-8" onPress={() => inputDigit(8)}>
8
</CalculatorKey>
<CalculatorKey className="key-9" onPress={() => this.inputDigit(9)}>
<CalculatorKey className="key-9" onPress={() => inputDigit(9)}>
9
</CalculatorKey>
</div>
</div>
<div className="operator-keys">
<CalculatorKey className="key-divide" onPress={() => this.performOperation('/')}>
<CalculatorKey className="key-divide" onPress={() => performOperation('/')}>
÷
</CalculatorKey>
<CalculatorKey className="key-multiply" onPress={() => this.performOperation('*')}>
<CalculatorKey className="key-multiply" onPress={() => performOperation('*')}>
×
</CalculatorKey>
<CalculatorKey className="key-subtract" onPress={() => this.performOperation('-')}>
<CalculatorKey className="key-subtract" onPress={() => performOperation('-')}>
</CalculatorKey>
<CalculatorKey className="key-add" onPress={() => this.performOperation('+')}>
<CalculatorKey className="key-add" onPress={() => performOperation('+')}>
+
</CalculatorKey>
<CalculatorKey className="key-equals" onPress={() => this.performOperation('=')}>
<CalculatorKey className="key-equals" onPress={() => performOperation('=')}>
=
</CalculatorKey>
</div>
</div>
</div>
</div>
);
}
}
});
export const CalculatorWrapper = () => {
export const CalculatorWrapper = React.forwardRef((props, ref) => {
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 }) => {
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%;

View File

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

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