Просмотр исходного кода

feat: 添加支持变量输入框

jiaxing.liao 3 дней назад
Родитель
Сommit
ea1af1da50

+ 0 - 121
apps/web/src/components/PromptEditor/plugins/VarLabel.tsx

@@ -1,121 +0,0 @@
-import { TextNode } from 'lexical'
-import type { EditorConfig, SerializedTextNode } from 'lexical'
-
-export type SerializedVarLabelNode = SerializedTextNode & {
-	type: 'var-label'
-	className: string
-	key?: string
-}
-
-function getVarLabelType(className: string) {
-	if (className.includes('env-label')) return 'env'
-	if (className.includes('sys-label')) return 'sys'
-	if (className.includes('node-label')) return 'node'
-	return 'custom'
-}
-
-function getDisplayText(rawText: string) {
-	if (rawText.startsWith('#{env.') && rawText.endsWith('}')) {
-		return rawText.slice(6, -1)
-	}
-
-	if (rawText.startsWith('#{sys.') && rawText.endsWith('}')) {
-		return rawText.slice(6, -1)
-	}
-
-	if (rawText.startsWith('#{') && rawText.endsWith('}')) {
-		const expression = rawText.slice(2, -1)
-		const splitIndex = expression.indexOf('.')
-		if (splitIndex > -1) {
-			return expression.slice(splitIndex + 1)
-		}
-		return expression
-	}
-
-	return rawText
-}
-
-function updateInnerPresentation(inner: HTMLElement, className: string, rawText: string) {
-	inner.className = 'var-label-inner'
-	inner.setAttribute('data-tag-type', getVarLabelType(className))
-	inner.textContent = getDisplayText(rawText)
-}
-
-export class VarLabelNode extends TextNode {
-	__className: string
-
-	static getType() {
-		return 'var-label'
-	}
-
-	static clone(node: VarLabelNode) {
-		return new VarLabelNode(node.__className, node.__text, node.__key)
-	}
-
-	static importJSON(serializedNode: SerializedTextNode): VarLabelNode {
-		const { className, text, key } = serializedNode as SerializedVarLabelNode
-		const node = new VarLabelNode(className, text, key)
-		node.setMode('token')
-		return node
-	}
-
-	constructor(className: string, text: string, key?: string) {
-		super(text, key)
-		this.__className = className
-	}
-
-	isTextEntity() {
-		return true
-	}
-
-	canInsertTextBefore() {
-		return false
-	}
-
-	canInsertTextAfter() {
-		return false
-	}
-
-	createDOM(config: EditorConfig) {
-		const dom = document.createElement('span')
-		const inner = super.createDOM(config)
-		dom.className = this.__className
-		dom.contentEditable = 'false'
-		updateInnerPresentation(inner, this.__className, this.__text)
-		dom.appendChild(inner)
-		return dom
-	}
-
-	updateDOM(prevNode: VarLabelNode, dom: ChildNode, config: EditorConfig) {
-		const inner = dom.firstChild
-		if (inner === null) {
-			return true
-		}
-
-		if (prevNode.__className !== this.__className) {
-			;(dom as HTMLElement).className = this.__className
-		}
-		;(dom as HTMLElement).contentEditable = 'false'
-
-		super.updateDOM(prevNode as this, inner as HTMLElement, config)
-		updateInnerPresentation(inner as HTMLElement, this.__className, this.__text)
-		return false
-	}
-
-	exportJSON(): SerializedVarLabelNode {
-		return {
-			...super.exportJSON(),
-			type: 'var-label',
-			className: this.__className,
-			version: 1
-		}
-	}
-}
-
-export function $isVarLabelNode(node: unknown): node is VarLabelNode {
-	return node instanceof VarLabelNode
-}
-
-export function $createVarLabelNode(className: string, text: string) {
-	return new VarLabelNode(className, text).setMode('token')
-}

+ 56 - 25
apps/web/src/components/PromptEditor/index.vue

@@ -163,62 +163,93 @@ function onChange(editorState: any) {
 	}
 }
 
-:deep(.var-label-inner) {
+:deep(.var-label-token) {
+	display: inline-flex;
+	align-items: center;
+	margin: 0 2px;
+	vertical-align: baseline;
+	position: relative;
+	top: -1px;
+	line-height: 1;
+}
+
+:deep(.var-label-content) {
 	display: inline-flex;
 	align-items: center;
 	height: 22px;
 	padding: 0 8px;
-	margin: 0 2px;
+	padding-left: 0;
 	border-radius: 6px;
 	border: 1px solid transparent;
-	font-size: 12px;
+	font-size: 14px;
 	line-height: 20px;
 	box-sizing: border-box;
-	vertical-align: middle;
 	gap: 4px;
+	vertical-align: middle;
 }
 
-:deep(.var-label-inner[data-tag-type='env']::before),
-:deep(.var-label-inner[data-tag-type='sys']::before) {
-	display: inline-flex;
-	align-items: center;
-	justify-content: center;
-	min-width: 18px;
-	height: 14px;
-	padding: 0 4px;
-	border-radius: 4px;
-	font-size: 10px;
-	line-height: 14px;
-	font-weight: 600;
-	background: currentColor;
-	color: #fff;
-	content: attr(data-tag-type);
-}
-
-:deep(.env-label .var-label-inner) {
+:deep(.env-label .var-label-content) {
 	color: #6366f1;
 	background: #eef2ff;
 	border-color: #c7d2fe;
 }
 
-:deep(.sys-label .var-label-inner) {
+:deep(.sys-label .var-label-content) {
 	color: #ea580c;
 	background: #fff7ed;
 	border-color: #fed7aa;
 }
 
-:deep(.node-label .var-label-inner) {
+:deep(.node-label .var-label-content) {
 	color: #0f766e;
 	background: #f0fdfa;
 	border-color: #99f6e4;
 }
 
-:deep(.custom-label .var-label-inner) {
+:deep(.custom-label .var-label-content) {
 	color: #334155;
 	background: #f8fafc;
 	border-color: #cbd5e1;
 }
 
+:deep(.var-select__item-prefix) {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	width: 20px;
+	height: 20px;
+	border-radius: 6px;
+	color: #fff;
+	flex-shrink: 0;
+	line-height: 1;
+	font-weight: 600;
+}
+
+:deep(.var-select__item-prefix.env) {
+	background: #6366f1;
+}
+
+:deep(.var-select__item-prefix.sys) {
+	background: #f97316;
+}
+
+:deep(.var-select__item-prefix.node) {
+	background: #0f766e;
+}
+
+:deep(.var-label-node-name) {
+	max-width: 120px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+:deep(.var-label-value),
+:deep(.var-label-separator),
+:deep(.var-label-node-name) {
+	line-height: 20px;
+}
+
 .editor-placeholder {
 	line-height: 30px;
 	color: #999;

apps/web/src/components/PromptEditor/plugins/AutoFocusPlugin.vue → apps/web/src/nodes/_base/PromptEditor/plugins/AutoFocusPlugin.vue


apps/web/src/components/PromptEditor/plugins/CustomText.ts → apps/web/src/nodes/_base/PromptEditor/plugins/CustomText.ts


+ 163 - 0
apps/web/src/nodes/_base/PromptEditor/plugins/VarLabel.tsx

@@ -0,0 +1,163 @@
+import { TextNode } from 'lexical'
+import type { EditorConfig, SerializedTextNode } from 'lexical'
+
+export type SerializedVarLabelNode = SerializedTextNode & {
+	type: 'var-label'
+	className: string
+	key?: string
+}
+
+type LabelInfo = {
+	type: 'env' | 'sys' | 'node' | 'custom'
+	value: string
+	nodeName?: string
+}
+
+function parseLabelInfo(rawText: string, className: string): LabelInfo {
+	if (rawText.startsWith('#{') && rawText.endsWith('}')) {
+		const expression = rawText.slice(2, -1)
+		const [prefix, ...rest] = expression.split('.')
+		const value = rest.join('.')
+
+		if (prefix === 'env' && value) {
+			return { type: 'env', value }
+		}
+
+		if (prefix === 'sys' && value) {
+			return { type: 'sys', value }
+		}
+
+		if (prefix && value) {
+			return {
+				type: 'node',
+				nodeName: prefix,
+				value
+			}
+		}
+	}
+
+	if (className.includes('env-label')) {
+		return { type: 'env', value: rawText }
+	}
+
+	if (className.includes('sys-label')) {
+		return { type: 'sys', value: rawText }
+	}
+
+	if (className.includes('node-label')) {
+		return { type: 'node', nodeName: 'node', value: rawText }
+	}
+
+	return { type: 'custom', value: rawText }
+}
+
+function createSpan(className: string, text: string) {
+	const span = document.createElement('span')
+	span.className = className
+	span.textContent = text
+	return span
+}
+
+function renderLabel(dom: HTMLElement, className: string, rawText: string) {
+	const info = parseLabelInfo(rawText, className)
+
+	dom.className = `${className} var-label-token`
+	dom.contentEditable = 'false'
+	dom.title =
+		info.type === 'node' && info.nodeName ? `${info.nodeName} / ${info.value}` : info.value || rawText
+
+	const container = document.createElement('span')
+	container.className = 'var-label-content'
+
+	if (info.type === 'env' || info.type === 'sys') {
+		const prefix = document.createElement('span')
+		prefix.className = `var-select__item-prefix ${info.type} text-6px`
+		prefix.appendChild(createSpan('', info.type.toUpperCase()))
+		container.appendChild(prefix)
+		container.appendChild(createSpan('var-label-value text-gray-600', info.value))
+		dom.replaceChildren(container)
+		return
+	}
+
+	if (info.type === 'node') {
+		const prefix = createSpan('var-select__item-prefix node text-10px', 'VAR')
+		container.appendChild(prefix)
+		container.appendChild(createSpan('var-label-node-name', info.nodeName || 'node'))
+		container.appendChild(createSpan('var-label-separator mx-2px text-gray-400', '/'))
+		container.appendChild(createSpan('var-label-value text-gray-600', info.value))
+		dom.replaceChildren(container)
+		return
+	}
+
+	container.appendChild(createSpan('var-label-value text-gray-600', info.value))
+	dom.replaceChildren(container)
+}
+
+export class VarLabelNode extends TextNode {
+	__className: string
+
+	static getType() {
+		return 'var-label'
+	}
+
+	static clone(node: VarLabelNode) {
+		return new VarLabelNode(node.__className, node.__text, node.__key)
+	}
+
+	static importJSON(serializedNode: SerializedTextNode): VarLabelNode {
+		const { className, text, key } = serializedNode as SerializedVarLabelNode
+		const node = new VarLabelNode(className, text, key)
+		node.setMode('token')
+		return node
+	}
+
+	constructor(className: string, text: string, key?: string) {
+		super(text, key)
+		this.__className = className
+	}
+
+	isTextEntity() {
+		return true
+	}
+
+	canInsertTextBefore() {
+		return false
+	}
+
+	canInsertTextAfter() {
+		return false
+	}
+
+	createDOM(_config: EditorConfig) {
+		const dom = document.createElement('span')
+		renderLabel(dom, this.__className, this.__text)
+		return dom
+	}
+
+	updateDOM(prevNode: VarLabelNode, dom: ChildNode, _config: EditorConfig) {
+		if (!(dom instanceof HTMLElement)) return true
+
+		if (prevNode.__className !== this.__className || prevNode.__text !== this.__text) {
+			renderLabel(dom, this.__className, this.__text)
+		}
+
+		return false
+	}
+
+	exportJSON(): SerializedVarLabelNode {
+		return {
+			...super.exportJSON(),
+			type: 'var-label',
+			className: this.__className,
+			version: 1
+		}
+	}
+}
+
+export function $isVarLabelNode(node: unknown): node is VarLabelNode {
+	return node instanceof VarLabelNode
+}
+
+export function $createVarLabelNode(className: string, text: string) {
+	return new VarLabelNode(className, text).setMode('token')
+}

apps/web/src/components/PromptEditor/plugins/VarLabelBlock.vue → apps/web/src/nodes/_base/PromptEditor/plugins/VarLabelBlock.vue


apps/web/src/components/PromptEditor/plugins/VarLaberPickerPlugin.vue → apps/web/src/nodes/_base/PromptEditor/plugins/VarLaberPickerPlugin.vue


apps/web/src/components/PromptEditor/utils.ts → apps/web/src/nodes/_base/PromptEditor/utils.ts


+ 1 - 1
apps/web/src/nodes/_base/VarInput.vue

@@ -5,7 +5,7 @@
 </template>
 
 <script setup lang="ts">
-import PromptEditor from '@/components/PromptEditor/index.vue'
+import PromptEditor from './PromptEditor/index.vue'
 
 const modelValue = defineModel<string>('modelValue')
 </script>

+ 6 - 0
apps/web/src/nodes/_base/VarSelect.vue

@@ -26,6 +26,12 @@
 								</span>
 								<span class="text-gray-600">{{ valueInfo.value }}</span>
 							</div>
+							<div v-else-if="valueInfo.type === 'sys'" class="flex gap-1 items-center truncate">
+								<span class="var-select__item-prefix sys text-6px">
+									<span>SYS</span>
+								</span>
+								<span class="text-gray-600">{{ valueInfo.value }}</span>
+							</div>
 							<div v-else class="truncate" :title="valueInfo.nodeName + ' / ' + valueInfo.value">
 								<span
 									class="var-select__item-prefix env text-10px"

+ 19 - 8
apps/web/src/nodes/src/http/setter.vue

@@ -164,7 +164,11 @@ const handleSaveAuthorization = () => {
 						placeholder="请选择"
 					>
 					</el-select>
-					<VarInput class="flex-1" v-model="formData.url" placeholder="URL..."></VarInput>
+					<VarInput
+						class="flex-1"
+						v-model="formData.url"
+						placeholder="请输入URL, 输入/选择变量"
+					></VarInput>
 				</div>
 			</el-form-item>
 
@@ -175,7 +179,7 @@ const handleSaveAuthorization = () => {
 				<el-table :data="headers" border>
 					<el-table-column align="center" prop="name" label="键">
 						<template #default="{ row }">
-							<VarInput v-model="row.name" variant="borderless" placeholder="输入" />
+							<VarInput v-model="row.name" variant="borderless" placeholder="输入/时选择变量" />
 						</template>
 					</el-table-column>
 					<el-table-column align="center" prop="value" label="值">
@@ -184,7 +188,7 @@ const handleSaveAuthorization = () => {
 								<VarInput
 									v-model="row.value"
 									variant="borderless"
-									placeholder="输入"
+									placeholder="输入/时选择变量"
 									@focus="handleAddHeader($index)"
 								/>
 								<IconButton
@@ -206,7 +210,7 @@ const handleSaveAuthorization = () => {
 				<el-table :data="params" border>
 					<el-table-column align="center" prop="name" label="键">
 						<template #default="{ row }">
-							<VarInput v-model="row.name" variant="borderless" placeholder="输入" />
+							<VarInput v-model="row.name" variant="borderless" placeholder="输入/时选择变量" />
 						</template>
 					</el-table-column>
 					<el-table-column align="center" prop="value" label="值">
@@ -215,7 +219,7 @@ const handleSaveAuthorization = () => {
 								<VarInput
 									v-model="row.value"
 									variant="borderless"
-									placeholder="输入"
+									placeholder="输入/时选择变量"
 									@focus="handleAddParam($index)"
 								/>
 								<IconButton
@@ -274,7 +278,7 @@ const handleSaveAuthorization = () => {
 								<VarInput
 									v-model="row.value"
 									variant="borderless"
-									placeholder="输入"
+									placeholder="输入/时选择变量"
 									@focus="handleAddBody($index)"
 								/>
 								<IconButton
@@ -291,11 +295,18 @@ const handleSaveAuthorization = () => {
 
 			<div v-if="['json', 'raw', 'binary'].includes(formData.body.type)" class="mb-12px">
 				<el-input
+					v-if="formData.body.type != 'binary'"
 					v-model="formData.body.data[0]!.value"
-					:type="formData.body.type != 'binary' ? 'textarea' : ''"
-					:placeholder="formData.body.type != 'binary' ? '请输入' : '请输入变量'"
+					type="textarea"
+					placeholder="请输入"
 					:autosize="{ minRows: 5, maxRows: 10 }"
 				/>
+				<VarInput
+					v-if="formData.body.type === 'binary'"
+					v-model="formData.body.data[0]!.value"
+					variant="borderless"
+					placeholder="输入/时选择变量"
+				/>
 			</div>
 
 			<el-form-item label="" label-position="top">

+ 2 - 2
apps/web/src/nodes/src/list/setter.vue

@@ -107,7 +107,7 @@ const handleChangeFilterCondition = (val: { value: string; type: string }) => {
 					<label class="text-14px font-bold text-gray-700">过滤条件</label>
 					<el-switch v-model="formData.filter_by.enabled" />
 				</div>
-				<div class="flex items-center gap-12px">
+				<div class="w-full flex items-center gap-12px">
 					<el-select :model-value="conditions?.comparison_operator" :options="[]" />
 					<VarInput :model-value="conditions?.right_value!" @change="handleChangeFilterCondition" />
 				</div>
@@ -140,7 +140,7 @@ const handleChangeFilterCondition = (val: { value: string; type: string }) => {
 				<div class="w-full flex items-center justify-between beautify">
 					<label class="text-14px font-bold text-gray-700">排序</label>
 				</div>
-				<div class="flex gap-12px">
+				<div class="w-full flex gap-12px">
 					<VarInput v-model="formData.order_by.key" />
 					<el-radio-group v-model="formData.order_by.value" size="small" class="w-160px">
 						<el-radio-button label="升序" value="asc" />