feat: 参数修改
parent
3df2e753c5
commit
85ed5e37fd
|
|
@ -74,22 +74,64 @@
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-value-rich-editor {
|
.param-value-rich-editor-shell {
|
||||||
|
position: relative;
|
||||||
|
min-height: var(--param-value-editor-min-height);
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor,
|
||||||
|
.param-value-rich-editor-highlight {
|
||||||
|
box-sizing: border-box;
|
||||||
min-height: var(--param-value-editor-min-height);
|
min-height: var(--param-value-editor-min-height);
|
||||||
max-height: 260px;
|
max-height: 260px;
|
||||||
padding: 6px 11px;
|
padding: 6px 11px;
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
|
font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5715;
|
line-height: 1.5715;
|
||||||
outline: none;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor-highlight {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor-highlight .param-value-template-token {
|
||||||
|
margin: 0 2px;
|
||||||
|
vertical-align: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent !important;
|
||||||
|
color: transparent !important;
|
||||||
|
caret-color: rgba(0, 0, 0, 0.88);
|
||||||
|
outline: none;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor::placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
-webkit-text-fill-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value-rich-editor::selection {
|
||||||
|
background: #bae0ff;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
-webkit-text-fill-color: rgba(0, 0, 0, 0.88);
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-value-rich-editor:hover {
|
.param-value-rich-editor:hover {
|
||||||
|
|
@ -101,16 +143,6 @@
|
||||||
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-value-rich-editor:empty::before {
|
|
||||||
color: rgba(0, 0, 0, 0.25);
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
.param-value-rich-editor .param-value-template-token {
|
|
||||||
margin: 0 2px;
|
|
||||||
vertical-align: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.param-boolean-segmented {
|
.param-boolean-segmented {
|
||||||
width: 176px;
|
width: 176px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button, Col, Drawer, Form, Input, Popconfirm, Popover, Row, Segmented, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd";
|
import { Button, Col, Drawer, Form, Input, Popconfirm, Popover, Row, Segmented, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd";
|
||||||
import { type CSSProperties, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { type ChangeEvent, type CSSProperties, type ReactNode, type UIEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons";
|
import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons";
|
||||||
import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
||||||
|
|
@ -54,49 +54,6 @@ const renderTemplateParamSegments = (text: string) => {
|
||||||
return segments;
|
return segments;
|
||||||
};
|
};
|
||||||
|
|
||||||
const readEditorText = (element: HTMLElement) => element.innerText.replace(/\r\n/g, "\n");
|
|
||||||
|
|
||||||
const getCaretTextOffset = (element: HTMLElement) => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || selection.rangeCount === 0 || !selection.anchorNode || !element.contains(selection.anchorNode)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const prefixRange = range.cloneRange();
|
|
||||||
prefixRange.selectNodeContents(element);
|
|
||||||
prefixRange.setEnd(range.endContainer, range.endOffset);
|
|
||||||
return prefixRange.toString().length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const restoreCaretTextOffset = (element: HTMLElement, offset: number) => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection) return;
|
|
||||||
|
|
||||||
const range = document.createRange();
|
|
||||||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
||||||
let remaining = offset;
|
|
||||||
let node = walker.nextNode();
|
|
||||||
|
|
||||||
while (node) {
|
|
||||||
const textLength = node.textContent?.length ?? 0;
|
|
||||||
if (remaining <= textLength) {
|
|
||||||
range.setStart(node, remaining);
|
|
||||||
range.collapse(true);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
remaining -= textLength;
|
|
||||||
node = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
range.selectNodeContents(element);
|
|
||||||
range.collapse(false);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TemplateParamValueEditor({
|
function TemplateParamValueEditor({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
|
@ -112,52 +69,43 @@ function TemplateParamValueEditor({
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
}) {
|
}) {
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const highlightRef = useRef<HTMLDivElement>(null);
|
||||||
const pendingCaretOffsetRef = useRef<number | null>(null);
|
|
||||||
const text = value === undefined || value === null ? "" : String(value);
|
const text = value === undefined || value === null ? "" : String(value);
|
||||||
const editorStyle = { "--param-value-editor-min-height": `${rows * 22 + 20}px` } as CSSProperties;
|
const editorStyle = { "--param-value-editor-min-height": `${rows * 22 + 20}px` } as CSSProperties;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const editor = editorRef.current;
|
const nextValue = event.target.value;
|
||||||
const caretOffset = pendingCaretOffsetRef.current;
|
|
||||||
if (!editor || caretOffset === null || document.activeElement !== editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreCaretTextOffset(editor, caretOffset);
|
|
||||||
pendingCaretOffsetRef.current = null;
|
|
||||||
}, [text]);
|
|
||||||
|
|
||||||
const handleInput = () => {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
pendingCaretOffsetRef.current = getCaretTextOffset(editor);
|
|
||||||
const nextValue = readEditorText(editor);
|
|
||||||
onChange?.(nextValue);
|
onChange?.(nextValue);
|
||||||
onTextChange?.(nextValue);
|
onTextChange?.(nextValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = (event: UIEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!highlightRef.current) return;
|
||||||
|
|
||||||
|
highlightRef.current.scrollTop = event.currentTarget.scrollTop;
|
||||||
|
highlightRef.current.scrollLeft = event.currentTarget.scrollLeft;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="param-value-rich-editor-shell" style={editorStyle}>
|
||||||
ref={editorRef}
|
<div ref={highlightRef} className="param-value-rich-editor-highlight" aria-hidden>
|
||||||
className="param-value-rich-editor"
|
{text ? (
|
||||||
contentEditable
|
<>
|
||||||
data-placeholder={placeholder}
|
{renderTemplateParamSegments(text)}
|
||||||
role="textbox"
|
<span className="param-value-editor-tail"> </span>
|
||||||
aria-multiline="true"
|
</>
|
||||||
spellCheck={false}
|
) : null}
|
||||||
style={editorStyle}
|
</div>
|
||||||
suppressContentEditableWarning
|
<Input.TextArea
|
||||||
tabIndex={0}
|
className="param-value-rich-editor"
|
||||||
onFocus={onFocus}
|
value={text}
|
||||||
onInput={handleInput}
|
placeholder={placeholder}
|
||||||
onPaste={(event) => {
|
rows={rows}
|
||||||
event.preventDefault();
|
spellCheck={false}
|
||||||
document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
|
onFocus={onFocus}
|
||||||
}}
|
onChange={handleChange}
|
||||||
>
|
onScroll={handleScroll}
|
||||||
{text ? renderTemplateParamSegments(text) : null}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue