feat: 参数修改
parent
3df2e753c5
commit
85ed5e37fd
|
|
@ -74,22 +74,64 @@
|
|||
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);
|
||||
max-height: 260px;
|
||||
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-size: 14px;
|
||||
line-height: 1.5715;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
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;
|
||||
-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 {
|
||||
|
|
@ -101,16 +143,6 @@
|
|||
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 {
|
||||
width: 176px;
|
||||
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 { 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 { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons";
|
||||
import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
||||
|
|
@ -54,49 +54,6 @@ const renderTemplateParamSegments = (text: string) => {
|
|||
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({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -112,52 +69,43 @@ function TemplateParamValueEditor({
|
|||
placeholder?: string;
|
||||
rows?: number;
|
||||
}) {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const pendingCaretOffsetRef = useRef<number | null>(null);
|
||||
const highlightRef = useRef<HTMLDivElement>(null);
|
||||
const text = value === undefined || value === null ? "" : String(value);
|
||||
const editorStyle = { "--param-value-editor-min-height": `${rows * 22 + 20}px` } as CSSProperties;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
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);
|
||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const nextValue = event.target.value;
|
||||
onChange?.(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 (
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="param-value-rich-editor"
|
||||
contentEditable
|
||||
data-placeholder={placeholder}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
spellCheck={false}
|
||||
style={editorStyle}
|
||||
suppressContentEditableWarning
|
||||
tabIndex={0}
|
||||
onFocus={onFocus}
|
||||
onInput={handleInput}
|
||||
onPaste={(event) => {
|
||||
event.preventDefault();
|
||||
document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
|
||||
}}
|
||||
>
|
||||
{text ? renderTemplateParamSegments(text) : null}
|
||||
<div className="param-value-rich-editor-shell" style={editorStyle}>
|
||||
<div ref={highlightRef} className="param-value-rich-editor-highlight" aria-hidden>
|
||||
{text ? (
|
||||
<>
|
||||
{renderTemplateParamSegments(text)}
|
||||
<span className="param-value-editor-tail"> </span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<Input.TextArea
|
||||
className="param-value-rich-editor"
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
spellCheck={false}
|
||||
onFocus={onFocus}
|
||||
onChange={handleChange}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue