Sfoglia il codice sorgente

pref: 优化列表节点配置

jiaxing.liao 1 mese fa
parent
commit
7ea9e8072d

+ 0 - 2
apps/web/components.d.ts

@@ -65,7 +65,6 @@ declare module 'vue' {
     ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
-    RunWorkflow: typeof import('./src/components/RunWorkflow/index.vue')['default']
     SearchDialog: typeof import('./src/components/SearchDialog/index.vue')['default']
     Sidebar: typeof import('./src/components/Sidebar/index.vue')['default']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
@@ -132,7 +131,6 @@ declare global {
   const ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
   const RouterLink: typeof import('vue-router')['RouterLink']
   const RouterView: typeof import('vue-router')['RouterView']
-  const RunWorkflow: typeof import('./src/components/RunWorkflow/index.vue')['default']
   const SearchDialog: typeof import('./src/components/SearchDialog/index.vue')['default']
   const Sidebar: typeof import('./src/components/Sidebar/index.vue')['default']
   const SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']

+ 7 - 1
apps/web/src/constant/index.ts

@@ -1,6 +1,6 @@
 /**
  * 变量类型选项
- * 值:string/number/boolean/object/array[string]/array[number]/array[boolean]/array[object]
+ * 值:string/number/boolean/object/array[string]/array[number]/array[boolean]/array[object]/array[file]
  */
 export const VARIABLE_TYPE_OPTIONS = [
 	{ label: 'String', value: 'string' },
@@ -77,5 +77,11 @@ export const VARIABLE_TYPE_OPERATORS = {
 	'array[object]': [
 		{ label: '为空', value: 'empty' },
 		{ label: '不为空', value: 'not_empty' }
+	],
+	'array[file]': [
+		{ label: '包含', value: 'contains' },
+		{ label: '不包含', value: 'not_contains' },
+		{ label: '为空', value: 'empty' },
+		{ label: '不为空', value: 'not_empty' }
 	]
 }

+ 3 - 1
apps/web/src/nodes/Interface.ts

@@ -191,6 +191,7 @@ export type NodeVariableType =
 	| 'array[number]'
 	| 'array[boolean]'
 	| 'array[object]'
+	| 'array[file]'
 
 /**
  * 节点变量值类型
@@ -342,7 +343,7 @@ export interface ConditionType {
 
 	/**
 	 * 变量类型
-	 * 可选值: string | number | boolean | object | array[string] | array[number] | array[boolean] | array[object]
+	 * 可选值: string | number | boolean | object | array[string] | array[number] | array[boolean] | array[object] | array[file]
 	 */
 	varType:
 		| 'string'
@@ -353,4 +354,5 @@ export interface ConditionType {
 		| 'array[number]'
 		| 'array[boolean]'
 		| 'array[object]'
+		| 'array[file]'
 }

+ 1 - 0
apps/web/src/nodes/src/list/index.ts

@@ -43,6 +43,7 @@ export type ListData = INodeDataBaseSchema & {
 				| 'array[number]'
 				| 'array[boolean]'
 				| 'array[object]'
+				| 'array[file]'
 		}>
 	}
 

+ 231 - 35
apps/web/src/nodes/src/list/setter.vue

@@ -1,13 +1,65 @@
 <script lang="ts" setup>
-import { computed } from 'vue'
-import VarSelect from '@/nodes/_base/VarSelect.vue'
+import { computed, watch } from 'vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
+import VarSelect from '@/nodes/_base/VarSelect.vue'
+import { VARIABLE_TYPE_OPERATORS } from '@/constant'
 import { useSetterModel } from '../_shared/useSetterModel'
 
+import type { ConditionType, NodeVariable, NodeVariableType } from '@/nodes/Interface'
 import type { ListData } from './index'
-import type { NodeVariableType } from '@/nodes/Interface'
 import type { NodeVar } from '@/types/var'
 
+type AllowedListInputType = 'array[string]' | 'array[number]' | 'array[boolean]' | 'array[file]'
+
+type FilterValueType = Extract<ConditionType['varType'], 'string' | 'number' | 'boolean'>
+
+type OperatorsType = ListData['filter_by']['conditions'][0]['comparison_operator']
+
+const ALLOWED_INPUT_TYPES: AllowedListInputType[] = [
+	'array[string]',
+	'array[number]',
+	'array[boolean]',
+	'array[file]'
+]
+
+const FILTER_VALUE_TYPE_MAP: Record<AllowedListInputType, FilterValueType> = {
+	'array[string]': 'string',
+	'array[number]': 'number',
+	'array[boolean]': 'boolean',
+	'array[file]': 'string'
+}
+
+const DEFAULT_OPERATOR_MAP: Record<FilterValueType, ConditionType['comparison_operator']> = {
+	string: 'contains',
+	number: '=',
+	boolean: 'is'
+}
+
+const OUTPUT_ITEM_TYPE_MAP: Record<AllowedListInputType, NodeVariableType> = {
+	'array[string]': 'string',
+	'array[number]': 'number',
+	'array[boolean]': 'boolean',
+	'array[file]': 'object'
+}
+
+const FILE_OPTIONS = [
+	{ label: 'ID', value: 'id' },
+	{ label: 'Name', value: 'name' },
+	{ label: 'ExtensionName', value: 'extensionName' },
+	{ label: 'Size', value: 'size' },
+	{ label: 'Path', value: 'path' }
+]
+
+const FILE_FILTER_FIELD = {
+	label: 'name',
+	value: 'name'
+} as const
+
+const BOOLEAN_OPTIONS = [
+	{ label: 'True', value: 'true' },
+	{ label: 'False', value: 'false' }
+]
+
 const props = defineProps<{
 	data: ListData
 }>()
@@ -44,37 +96,109 @@ const conditions = computed({
 	}
 })
 
-const handleInputVarChange = (val: { value: string; type: string }) => {
-	if (inputVar.value) {
-		inputVar.value.value = val.value
-		inputVar.value.type = val.type as NodeVariableType
-	}
+const currentInputType = computed<AllowedListInputType | undefined>(() => {
+	const type = inputVar.value?.type
+	return type && ALLOWED_INPUT_TYPES.includes(type as AllowedListInputType)
+		? (type as AllowedListInputType)
+		: undefined
+})
+
+const currentFilterValueType = computed<FilterValueType>(() => {
+	if (!currentInputType.value) return 'string'
+	return FILTER_VALUE_TYPE_MAP[currentInputType.value]
+})
+
+const isFileInputType = computed(() => currentInputType.value === 'array[file]')
+
+const operatorOptions = computed(() => {
+	return VARIABLE_TYPE_OPERATORS[currentFilterValueType.value] ?? []
+})
+
+const getOperatorsByFilterType = (type: FilterValueType) => {
+	return VARIABLE_TYPE_OPERATORS[type] ?? []
+}
+
+const normalizeBooleanValue = (value?: string) => {
+	return value === 'false' ? 'false' : 'true'
+}
+
+const syncOutputs = (type?: AllowedListInputType) => {
+	if (!type) return
+
+	const resultType = type
+	const itemType = OUTPUT_ITEM_TYPE_MAP[type]
+	const outputTemplates: Array<Pick<NodeVariable, 'name' | 'describe' | 'type'>> = [
+		{ name: 'result', describe: '过滤结果', type: resultType },
+		{ name: 'first_record', describe: '第一条记录', type: itemType },
+		{ name: 'last_record', describe: '最后一条记录', type: itemType }
+	]
+
+	formData.value.outputs = outputTemplates.map((template, index) => ({
+		...(formData.value.outputs?.[index] || {}),
+		...template
+	}))
 }
 
-const isDataVarType = (type?: string) => {
-	if (!type) return false
+const syncConditionByInputType = (type?: AllowedListInputType) => {
+	if (!type) return
+
+	const nextVarType = FILTER_VALUE_TYPE_MAP[type]
+	const nextOperators = getOperatorsByFilterType(nextVarType)
+	const currentOperator = conditions.value?.comparison_operator
+	const nextOperator = nextOperators.some((item) => item.value === currentOperator)
+		? currentOperator
+		: DEFAULT_OPERATOR_MAP[nextVarType]
 
-	return ['string', 'number', 'boolean', 'object'].includes(type) || /^array\[[^\]]+\]$/.test(type)
+	conditions.value = {
+		...conditions.value,
+		comparison_operator: nextOperator as OperatorsType,
+		right_value:
+			nextVarType === 'boolean'
+				? normalizeBooleanValue(conditions.value?.right_value)
+				: (conditions.value?.right_value ?? ''),
+		left_value: type === 'array[file]' ? FILE_FILTER_FIELD.value : '',
+		varType: nextVarType
+	}
 }
 
+watch(
+	currentInputType,
+	(type) => {
+		syncOutputs(type)
+		syncConditionByInputType(type)
+	},
+	{ immediate: true }
+)
+
 const filterVarFn = (list: NodeVar[]) => {
 	return list
 		.map((group) => ({
 			...group,
-			variableList: group.variableList.filter((item) => isDataVarType(item.type))
+			variableList: group.variableList.filter((item) =>
+				ALLOWED_INPUT_TYPES.includes(item.type as AllowedListInputType)
+			)
 		}))
 		.filter((group) => group.variableList.length)
 }
 
-const handleChangeFilterCondition = (val: { value: string; type: string }) => {
+const handleInputVarChange = (val: { value: string; type: string }) => {
+	const nextType = val.type as NodeVariableType
+	const nextInputVar: NodeVariable = {
+		...(inputVar.value || { name: '' }),
+		value: val.value,
+		type: nextType
+	}
+
+	inputVar.value = nextInputVar
+	if (ALLOWED_INPUT_TYPES.includes(nextType as AllowedListInputType)) {
+		syncOutputs(nextType as AllowedListInputType)
+		syncConditionByInputType(nextType as AllowedListInputType)
+	}
+}
+
+const handleLeftChange = (val: string) => {
 	if (conditions.value) {
-		conditions.value.right_value = val.value
-	} else if (val) {
-		conditions.value = {
-			comparison_operator: 'contains',
-			right_value: '',
-			varType: 'string'
-		}
+		conditions.value.varType = val === 'size' ? 'number' : 'string'
 	}
 }
 </script>
@@ -84,7 +208,7 @@ const handleChangeFilterCondition = (val: { value: string; type: string }) => {
 		<el-form label-width="50px">
 			<el-form-item label="" label-position="top">
 				<div class="w-full flex items-center justify-between beautify">
-					<label class="text-14px font-bold text-gray-700">输入</label>
+					<label class="text-14px font-bold text-gray-700">输入变量</label>
 				</div>
 				<VarSelect
 					:model-value="inputVar?.value"
@@ -99,19 +223,50 @@ 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="w-full flex items-center gap-12px">
-					<el-select
-						:model-value="conditions?.comparison_operator"
-						:options="[]"
-						style="width: 120px"
-					/>
-					<VarInput :model-value="conditions?.right_value!" @change="handleChangeFilterCondition" />
+
+				<div v-if="conditions && formData.filter_by.enabled" class="w-full flex flex-col gap-12px">
+					<div v-if="isFileInputType" class="w-full">
+						<el-select
+							v-model="conditions.left_value"
+							:options="FILE_OPTIONS"
+							@change="handleLeftChange"
+						/>
+					</div>
+
+					<div class="w-full flex items-center gap-12px">
+						<el-select
+							v-model="conditions.comparison_operator"
+							:options="operatorOptions"
+							style="width: 120px"
+						/>
+
+						<el-radio-group
+							v-if="currentFilterValueType === 'boolean'"
+							v-model="conditions.right_value"
+							class="list-boolean-group"
+						>
+							<el-radio-button
+								v-for="option in BOOLEAN_OPTIONS"
+								:key="option.value"
+								:label="option.label"
+								:value="option.value"
+								class="list-boolean-group__item"
+							/>
+						</el-radio-group>
+
+						<VarInput
+							v-else
+							v-model="conditions.right_value"
+							class="flex-1"
+							placeholder="键入 '/' 键快速插入变量"
+						/>
+					</div>
 				</div>
 			</el-form-item>
 
 			<el-form-item label="" label-position="top">
-				<div class="w-full flex items-center justify-between beautify">
-					<label class="text-14px font-bold text-gray-700">取第N项</label>
+				<div class="w-full flex items-center justify-between">
+					<label class="text-14px font-bold text-gray-700">取第 N 项</label>
 					<el-switch v-model="formData.extract_by.enabled" />
 				</div>
 				<div class="w-full">
@@ -120,24 +275,29 @@ const handleChangeFilterCondition = (val: { value: string; type: string }) => {
 			</el-form-item>
 
 			<el-form-item label="" label-position="top">
-				<div class="w-full flex items-center justify-between beautify">
-					<label class="text-14px font-bold text-gray-700">取前N项</label>
+				<div class="w-full flex items-center justify-between">
+					<label class="text-14px font-bold text-gray-700">取前 N 项</label>
 					<el-switch v-model="formData.limit.enabled" />
 				</div>
 				<div class="w-full flex gap-12px">
 					<el-input-number v-model="formData.limit.size" :min="1" :max="20" />
-					<div class="flex-1">
+					<div class="flex-1 pr-10px">
 						<el-slider v-model="formData.limit.size" :min="1" :max="20" />
 					</div>
 				</div>
 			</el-form-item>
 
 			<el-form-item label="" label-position="top">
-				<div class="w-full flex items-center justify-between beautify">
+				<div class="w-full flex items-center justify-between">
 					<label class="text-14px font-bold text-gray-700">排序</label>
 				</div>
 				<div class="w-full flex gap-12px">
-					<VarInput v-model="formData.order_by.key" />
+					<el-select
+						v-if="isFileInputType"
+						v-model="formData.order_by.key"
+						:options="FILE_OPTIONS"
+						style=""
+					/>
 					<el-radio-group v-model="formData.order_by.value" size="small" class="w-160px">
 						<el-radio-button label="升序" value="asc" />
 						<el-radio-button label="降序" value="desc" />
@@ -147,3 +307,39 @@ const handleChangeFilterCondition = (val: { value: string; type: string }) => {
 		</el-form>
 	</el-scrollbar>
 </template>
+
+<style scoped lang="less">
+:deep(.el-form-item) {
+	padding-bottom: 10px;
+	margin-bottom: 8px;
+	border-bottom: 1px solid #eee;
+}
+
+.list-filter-field__tag {
+	display: inline-flex;
+	align-items: center;
+	padding: 6px 10px;
+	border-radius: 10px;
+	background: #fff;
+	color: var(--el-color-primary);
+	font-size: 14px;
+	line-height: 1;
+}
+
+.list-boolean-group {
+	display: flex;
+	width: 100%;
+}
+
+.list-boolean-group__item {
+	flex: 1;
+}
+
+:deep(.list-boolean-group .el-radio-button) {
+	flex: 1;
+}
+
+:deep(.list-boolean-group .el-radio-button__inner) {
+	width: 100%;
+}
+</style>

+ 1 - 0
apps/web/src/types/var.d.ts

@@ -7,6 +7,7 @@ export type VarType =
 	| 'array[number]'
 	| 'array[boolean]'
 	| 'array[object]'
+	| 'array[file]'
 
 export interface NodeVarItem {
 	expression: string