UnisKB/ui/src/views/application-workflow/component/DropdownMenu.vue

374 lines
12 KiB
Vue
Raw Normal View History

2025-06-03 08:08:49 +00:00
<template>
2025-06-26 08:36:08 +00:00
<div v-show="show" class="workflow-dropdown-menu border border-r-6">
2025-06-03 08:08:49 +00:00
<el-tabs v-model="activeName" class="workflow-dropdown-tabs">
2025-07-01 13:22:21 +00:00
<div style="display: flex; width: 100%; justify-content: center" class="mb-12">
<el-input
v-model="search_text"
class="mr-12 ml-12"
:placeholder="$t('views.applicationWorkflow.searchBar.placeholder')"
>
2025-06-03 08:08:49 +00:00
<template #suffix>
<el-icon class="el-input__icon"><search /></el-icon>
</template>
</el-input>
</div>
<el-tab-pane :label="$t('views.applicationWorkflow.baseComponent')" name="base">
<el-scrollbar height="400">
<div v-if="filter_menu_nodes.length > 0">
2025-06-30 10:06:18 +00:00
<template v-for="(node, index) in filter_menu_nodes" :key="index">
2025-07-01 13:22:21 +00:00
<el-text type="info" size="small" class="color-secondary ml-12">{{
node.label
2025-07-03 11:34:36 +00:00
}}</el-text>
2025-06-30 10:06:18 +00:00
<div class="flex-wrap mt-8">
<template v-for="(item, index) in node.list" :key="index">
<el-popover placement="right" :width="280">
<template #reference>
2025-07-03 11:34:36 +00:00
<div class="list-item flex align-center border border-r-6 mb-12 p-8-12 cursor ml-12"
style="width: 39%" @click.stop="clickNodes(item)" @mousedown.stop="onmousedown(item)">
2025-07-01 13:22:21 +00:00
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="32"
/>
2025-06-30 10:06:18 +00:00
<div class="lighter">{{ item.label }}</div>
</div>
</template>
<template #default>
<div class="flex align-center mb-8">
2025-07-01 13:22:21 +00:00
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="32"
/>
2025-06-30 10:06:18 +00:00
<div class="lighter color-text-primary">{{ item.label }}</div>
</div>
2025-07-01 13:22:21 +00:00
<el-text type="info" size="small" class="color-secondary lighter">{{
item.text
2025-07-03 11:34:36 +00:00
}}</el-text>
2025-06-30 10:06:18 +00:00
</template>
</el-popover>
</template>
2025-06-03 08:08:49 +00:00
</div>
</template>
</div>
<div v-else class="ml-16 mt-8">
<el-text type="info">{{ $t('views.applicationWorkflow.tip.noData') }}</el-text>
</div>
</el-scrollbar>
</el-tab-pane>
2025-06-26 12:25:24 +00:00
<el-tab-pane :label="$t('views.tool.title')" name="tool">
2025-06-03 08:08:49 +00:00
<el-scrollbar height="400">
2025-07-03 11:34:36 +00:00
<!-- 共享工具 -->
<el-collapse expand-icon-position="left">
<el-collapse-item name="shared" :icon="CaretRight">
<template #title>
<div class="flex align-center">
<AppIcon iconName="app-shared-active" style="font-size: 20px" class="color-primary"></AppIcon>
<span class="ml-8 lighter">{{ $t('views.shared.shared_tool') }}</span>
</div>
</template>
<NodeContent :list="sharedToolList" @clickNodes="(val: any) => clickNodes(toolLibNode, val, 'tool')"
@onmousedown="(val: any) => onmousedown(toolLibNode, val, 'tool')" />
</el-collapse-item>
</el-collapse>
2025-06-03 08:08:49 +00:00
2025-07-03 11:34:36 +00:00
<el-tree :data="toolTreeData" node-key="id"
:props="{ children: 'children', isLeaf: 'isLeaf', class: getNodeClass }" lazy :load="loadNode">
<template #default="{ data, node }">
<NodeContent v-if="!data._fake" :data="data" :node="node"
@clickNodes="(val: any) => clickNodes(toolLibNode, val, 'tool')"
@onmousedown="(val: any) => onmousedown(toolLibNode, val, 'tool')" />
</template>
</el-tree>
2025-06-03 08:08:49 +00:00
</el-scrollbar>
</el-tab-pane>
<el-tab-pane :label="$t('views.application.title')" name="application">
<el-scrollbar height="400">
2025-07-03 11:34:36 +00:00
<el-tree :data="applicationTreeData" node-key="id"
:props="{ children: 'children', isLeaf: 'isLeaf', class: getNodeClass }" lazy :load="loadNode">
<template #default="{ data, node }">
<NodeContent v-if="!data._fake" :data="data" :node="node"
@clickNodes="(val: any) => clickNodes(applicationNode, val, 'application')"
@onmousedown="(val: any) => onmousedown(applicationNode, val, 'application')" />
2025-06-03 08:08:49 +00:00
</template>
2025-07-03 11:34:36 +00:00
</el-tree>
2025-06-03 08:08:49 +00:00
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
2025-07-03 11:34:36 +00:00
import { menuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
2025-06-03 08:08:49 +00:00
import { iconComponent } from '@/workflow/icons/utils'
2025-07-03 11:34:36 +00:00
import ToolApi from '@/api/tool/tool'
2025-06-03 08:08:49 +00:00
import { isWorkFlow } from '@/utils/application'
2025-07-03 11:34:36 +00:00
import useStore from '@/stores'
import NodeContent from './NodeContent.vue'
import { SourceTypeEnum } from '@/enums/common'
import sharedWorkspaceApi from '@/api/shared-workspace'
import { CaretRight } from '@element-plus/icons-vue'
import ApplicationApi from '@/api/application/application'
2025-06-03 08:08:49 +00:00
const search_text = ref<string>('')
const props = defineProps({
show: {
type: Boolean,
2025-06-27 09:35:01 +00:00
default: false,
2025-06-03 08:08:49 +00:00
},
id: {
type: String,
2025-06-27 09:35:01 +00:00
default: '',
2025-06-03 08:08:49 +00:00
},
2025-06-27 09:35:01 +00:00
workflowRef: Object,
2025-06-03 08:08:49 +00:00
})
2025-07-03 11:34:36 +00:00
const { folder } = useStore()
2025-06-03 08:08:49 +00:00
const emit = defineEmits(['clickNodes', 'onmousedown'])
const loading = ref(false)
const activeName = ref('base')
const filter_menu_nodes = computed(() => {
2025-07-01 13:22:21 +00:00
if (!search_text.value) return menuNodes
const searchTerm = search_text.value.toLowerCase()
2025-07-01 08:21:46 +00:00
return menuNodes.reduce((result: any[], item) => {
2025-07-01 13:22:21 +00:00
const filteredList = item.list.filter((listItem) =>
listItem.label.toLowerCase().includes(searchTerm),
)
2025-06-30 10:06:18 +00:00
if (filteredList.length) {
2025-07-01 13:22:21 +00:00
result.push({ ...item, list: filteredList })
2025-06-30 10:06:18 +00:00
}
2025-07-01 13:22:21 +00:00
return result
}, [])
})
2025-06-27 02:30:06 +00:00
function clickNodes(item: any, data?: any, type?: string) {
2025-06-03 08:08:49 +00:00
if (data) {
item['properties']['stepName'] = data.name
2025-06-26 12:25:24 +00:00
if (type == 'tool') {
2025-06-03 08:08:49 +00:00
item['properties']['node_data'] = {
...data,
2025-06-26 12:25:24 +00:00
tool_lib_id: data.id,
2025-06-03 08:08:49 +00:00
input_field_list: data.input_field_list.map((field: any) => ({
...field,
2025-06-27 09:35:01 +00:00
value: field.source == 'reference' ? [] : '',
})),
2025-06-03 08:08:49 +00:00
}
}
if (type == 'application') {
if (isWorkFlow(data.type)) {
const nodeData = data.work_flow.nodes[0].properties.node_data
const fileUploadSetting = nodeData.file_upload_setting
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id,
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
...(!fileUploadSetting
? {}
: {
2025-07-03 11:34:36 +00:00
...(fileUploadSetting.document ? { document_list: [] } : {}),
...(fileUploadSetting.image ? { image_list: [] } : {}),
...(fileUploadSetting.audio ? { audio_list: [] } : {}),
}),
2025-06-03 08:08:49 +00:00
}
} else {
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
2025-06-27 09:35:01 +00:00
application_id: data.id,
2025-06-03 08:08:49 +00:00
}
}
}
}
props.workflowRef?.addNode(item)
emit('clickNodes', item)
}
2025-06-27 02:30:06 +00:00
function onmousedown(item: any, data?: any, type?: string) {
2025-06-03 08:08:49 +00:00
if (data) {
item['properties']['stepName'] = data.name
2025-06-26 12:25:24 +00:00
if (type == 'tool') {
2025-06-03 08:08:49 +00:00
item['properties']['node_data'] = {
...data,
2025-06-26 12:25:24 +00:00
tool_lib_id: data.id,
2025-06-03 08:08:49 +00:00
input_field_list: data.input_field_list.map((field: any) => ({
...field,
2025-06-27 09:35:01 +00:00
value: field.source == 'reference' ? [] : '',
})),
2025-06-03 08:08:49 +00:00
}
}
if (type == 'application') {
if (isWorkFlow(data.type)) {
const nodeData = data.work_flow.nodes[0].properties.node_data
const fileUploadSetting = nodeData.file_upload_setting
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id,
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
...(!fileUploadSetting
? {}
: {
2025-07-03 11:34:36 +00:00
...(fileUploadSetting.document ? { document_list: [] } : {}),
...(fileUploadSetting.image ? { image_list: [] } : {}),
...(fileUploadSetting.audio ? { audio_list: [] } : {}),
}),
2025-06-03 08:08:49 +00:00
}
} else {
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
2025-06-27 09:35:01 +00:00
application_id: data.id,
2025-06-03 08:08:49 +00:00
}
}
}
}
props.workflowRef?.onmousedown(item)
emit('onmousedown', item)
}
2025-07-03 11:34:36 +00:00
function getNodeClass(data: any) {
return data._fake ? 'tree-node--hidden' : ''
}
const loadNode = async (node: any, resolve: (children: any[]) => void) => {
if (node.level === 0) return resolve([])
try {
let folders
if (activeName.value === 'tool') {
const res = await ToolApi.getToolList({ folder_id: node.data.id })
node.data.cardList = res.data.tools
folders = res.data?.folders
} else {
const res = await ApplicationApi.getAllApplication({ folder_id: node.data.id })
node.data.cardList = res.data.filter(item => item.resource_type === "application")
folders = res.data.filter(item => item.resource_type === "folder")
}
const children = folders.map(f => ({
...f,
children: [],
isLeaf: false,
}))
if (folders.length === 0 && node.data.cardList.length > 0) {
// 插一个假子节点,确保树节点是“可折叠”的
children.push({
id: `__placeholder__${node.data.id}`,
isLeaf: true,
_fake: true,
})
}
resolve(children)
} catch (e: any) {
resolve([]) // 失败也要 resolve否则树会卡住
}
}
const toolTreeData = ref<any[]>([])
function getToolFolder() {
folder.asyncGetFolder(SourceTypeEnum.TOOL, {}, loading).then((res: any) => {
toolTreeData.value = res.data
})
}
const sharedToolList = ref<any[]>([])
async function getShareTool() {
try {
const res = await sharedWorkspaceApi.getToolList(loading)
sharedToolList.value = res.data
} catch (error: any) {
console.error(error)
}
}
const applicationTreeData = ref<any[]>([])
function getApplicationFolder() {
folder.asyncGetFolder(SourceTypeEnum.APPLICATION, {}, loading).then((res: any) => {
applicationTreeData.value = res.data
})
2025-06-03 08:08:49 +00:00
}
onMounted(() => {
2025-07-03 11:34:36 +00:00
getShareTool()
getToolFolder()
getApplicationFolder()
2025-06-03 08:08:49 +00:00
})
</script>
<style lang="scss" scoped>
.workflow-dropdown-menu {
-moz-user-select: none; /* Firefox */
-webkit-user-select: none; /* WebKit内核 */
-ms-user-select: none; /* IE10及以后 */
-khtml-user-select: none; /* 早期浏览器 */
-o-user-select: none; /* Opera */
user-select: none; /* CSS3属性 */
position: absolute;
top: 49px;
2025-07-01 13:22:21 +00:00
right: 16px;
2025-06-03 08:08:49 +00:00
z-index: 99;
2025-06-30 10:06:18 +00:00
width: 400px;
2025-06-03 08:08:49 +00:00
box-shadow: 0px 4px 8px 0px var(--app-text-color-light-1);
background: #ffffff;
padding-bottom: 8px;
.title {
padding: 12px 12px 4px;
}
.workflow-dropdown-item {
&:hover {
background: var(--app-text-color-light-1);
}
}
2025-07-03 11:34:36 +00:00
.list-item {
&:hover {
border-color: var(--el-color-primary);
}
}
:deep(.el-collapse) {
border-top-width: 0;
.el-collapse-item__header {
height: 40px;
gap: 0;
.el-collapse-item__arrow {
font-size: 16px;
color: var(--app-text-color-secondary);
padding: 6px;
}
}
.el-collapse-item__content {
padding: 0 12px 16px 12px;
.list {
margin-top: 0;
transform: none;
}
}
}
:deep(.el-tree-node):focus>.el-tree-node__content {
background: transparent;
}
:deep(.el-tree-node__content) {
height: auto;
align-items: baseline;
&:hover {
background: transparent;
}
}
:deep(.tree-node--hidden) {
display: none !important;
}
2025-06-03 08:08:49 +00:00
}
</style>