refactor: dropdownMenu

This commit is contained in:
teukkk 2025-07-11 10:50:34 +08:00
parent 2b74b8415f
commit c3ecddcd1b
2 changed files with 143 additions and 240 deletions

View File

@ -1,14 +1,13 @@
<template> <template>
<div v-show="show" class="workflow-dropdown-menu border border-r-6 white-bg"> <div v-show="show" class="workflow-dropdown-menu border border-r-6 white-bg">
<el-tabs v-model="activeName" class="workflow-dropdown-tabs"> <el-tabs v-model="activeName" class="workflow-dropdown-tabs">
<div style="display: flex; width: 100%; justify-content: center" class="mb-12"> <div v-show="activeName === 'base'" style="display: flex; width: 100%; justify-content: center" class="mb-12 mt-12">
<el-input <el-input v-model="search_text" class="mr-12 ml-12"
v-model="search_text" :placeholder="$t('views.applicationWorkflow.searchBar.placeholder')">
class="mr-12 ml-12"
:placeholder="$t('views.applicationWorkflow.searchBar.placeholder')"
>
<template #suffix> <template #suffix>
<el-icon class="el-input__icon"><search /></el-icon> <el-icon class="el-input__icon">
<search />
</el-icon>
</template> </template>
</el-input> </el-input>
</div> </div>
@ -19,37 +18,25 @@
<template v-for="(node, index) in filter_menu_nodes" :key="index"> <template v-for="(node, index) in filter_menu_nodes" :key="index">
<el-text type="info" size="small" class="color-secondary ml-12">{{ <el-text type="info" size="small" class="color-secondary ml-12">{{
node.label node.label
}}</el-text> }}</el-text>
<div class="flex-wrap mt-8"> <div class="flex-wrap" style="gap: 12px; padding: 12px;">
<template v-for="(item, index) in node.list" :key="index"> <template v-for="(item, index) in node.list" :key="index">
<el-popover placement="right" :width="280"> <el-popover placement="right" :width="280" :show-after="500">
<template #reference> <template #reference>
<div <div class="list-item flex align-center border border-r-6 p-8-12 cursor"
class="list-item flex align-center border border-r-6 mb-12 p-8-12 cursor ml-12" style="width: calc(50% - 6px)" @click.stop="clickNodes(item)" @mousedown.stop="onmousedown(item)">
style="width: 39%" <component :is="iconComponent(`${item.type}-icon`)" class="mr-8" :size="32" />
@click.stop="clickNodes(item)"
@mousedown.stop="onmousedown(item)"
>
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="32"
/>
<div class="lighter">{{ item.label }}</div> <div class="lighter">{{ item.label }}</div>
</div> </div>
</template> </template>
<template #default> <template #default>
<div class="flex align-center mb-8"> <div class="flex align-center mb-8">
<component <component :is="iconComponent(`${item.type}-icon`)" class="mr-8" :size="32" />
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="32"
/>
<div class="lighter color-text-primary">{{ item.label }}</div> <div class="lighter color-text-primary">{{ item.label }}</div>
</div> </div>
<el-text type="info" size="small" class="color-secondary lighter">{{ <el-text type="info" size="small" class="color-secondary lighter">{{
item.text item.text
}}</el-text> }}</el-text>
</template> </template>
</el-popover> </el-popover>
</template> </template>
@ -61,68 +48,34 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane> </el-tab-pane>
<!-- 工具 -->
<el-tab-pane :label="$t('views.tool.title')" name="tool"> <el-tab-pane :label="$t('views.tool.title')" name="tool">
<el-scrollbar height="400"> <LayoutContainer>
<!-- 共享工具 --> <template #left>
<el-collapse expand-icon-position="left" v-if="user.isEE()"> <folder-tree :source="SourceTypeEnum.TOOL" :data="toolTreeData" :currentNodeKey="folder.currentFolder?.id"
<el-collapse-item name="shared" :icon="CaretRight"> @handleNodeClick="folderClickHandle" :shareTitle="$t('views.shared.shared_tool')"
<template #title> :showShared="user.isEE()" class="p-8" :canOperation="false" />
<div class="flex align-center"> </template>
<AppIcon <el-scrollbar height="450">
iconName="app-shared-active" <NodeContent :list="toolList" @clickNodes="(val: any) => clickNodes(toolLibNode, val, 'tool')"
style="font-size: 20px" @onmousedown="(val: any) => onmousedown(toolLibNode, val, 'tool')" />
class="color-primary" </el-scrollbar>
></AppIcon> </LayoutContainer>
<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>
<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>
</el-scrollbar>
</el-tab-pane> </el-tab-pane>
<!-- 应用 -->
<el-tab-pane :label="$t('views.application.title')" name="application"> <el-tab-pane :label="$t('views.application.title')" name="application">
<el-scrollbar height="400"> <LayoutContainer>
<el-tree <template #left>
:data="applicationTreeData" <folder-tree :source="SourceTypeEnum.APPLICATION" :data="applicationTreeData"
node-key="id" :currentNodeKey="folder.currentFolder?.id" @handleNodeClick="folderClickHandle" class="p-8"
:props="{ children: 'children', isLeaf: 'isLeaf', class: getNodeClass }" :canOperation="false" />
lazy </template>
:load="loadNode" <el-scrollbar height="450">
> <NodeContent :list="applicationList"
<template #default="{ data, node }"> @clickNodes="(val: any) => clickNodes(applicationNode, val, 'application')"
<NodeContent @onmousedown="(val: any) => onmousedown(applicationNode, val, 'application')" />
v-if="!data._fake" </el-scrollbar>
:data="data" </LayoutContainer>
:node="node"
@clickNodes="(val: any) => clickNodes(applicationNode, val, 'application')"
@onmousedown="(val: any) => onmousedown(applicationNode, val, 'application')"
/>
</template>
</el-tree>
</el-scrollbar>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -176,7 +129,6 @@ const filter_menu_nodes = computed(() => {
}, []) }, [])
}) })
function clickNodes(item: any, data?: any, type?: string) { function clickNodes(item: any, data?: any, type?: string) {
console.log('clickNodes', item, data, type)
if (data) { if (data) {
item['properties']['stepName'] = data.name item['properties']['stepName'] = data.name
if (type == 'tool') { if (type == 'tool') {
@ -237,52 +189,16 @@ function onmousedown(item: any, data?: any, type?: string) {
emit('onmousedown', item) emit('onmousedown', item)
} }
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[]>([]) const toolTreeData = ref<any[]>([])
function getToolFolder() { const toolList = ref<any[]>([])
folder.asyncGetFolder(SourceTypeEnum.TOOL, {}, loading).then((res: any) => { const sharedToolList = ref<any[]>([])
toolTreeData.value = res.data
}) async function getToolFolder() {
const res: any = await folder.asyncGetFolder(SourceTypeEnum.TOOL, {}, loading)
toolTreeData.value = res.data
folder.setCurrentFolder(res.data?.[0] || {})
} }
const sharedToolList = ref<any[]>([])
async function getShareTool() { async function getShareTool() {
try { try {
const res = await sharedWorkspaceApi.getToolList(loading) const res = await sharedWorkspaceApi.getToolList(loading)
@ -292,19 +208,47 @@ async function getShareTool() {
} }
} }
async function getToolList() {
if (folder.currentFolder.id === "share") {
toolList.value = sharedToolList.value
} else {
const res = await ToolApi.getToolList({ folder_id: folder.currentFolder?.id || user.getWorkspaceId() })
toolList.value = res.data.tools
}
}
const applicationTreeData = ref<any[]>([]) const applicationTreeData = ref<any[]>([])
const applicationList = ref<any[]>([])
function getApplicationFolder() { function getApplicationFolder() {
folder.asyncGetFolder(SourceTypeEnum.APPLICATION, {}, loading).then((res: any) => { folder.asyncGetFolder(SourceTypeEnum.APPLICATION, {}, loading).then((res: any) => {
applicationTreeData.value = res.data applicationTreeData.value = res.data
folder.setCurrentFolder(res.data?.[0] || {})
}) })
} }
onMounted(() => { async function getApplicationList() {
if (user.isEE()) { const res = await ApplicationApi.getAllApplication({ folder_id: folder.currentFolder?.id || user.getWorkspaceId() })
getShareTool() applicationList.value = res.data.filter((item) => item.resource_type === 'application')
}
function folderClickHandle(row: any) {
folder.setCurrentFolder(row)
if (activeName.value === 'tool') {
getToolList();
} else {
getApplicationList()
} }
getToolFolder() }
onMounted(async () => {
if (user.isEE()) {
await getShareTool()
}
await getToolFolder()
getToolList()
getApplicationFolder() getApplicationFolder()
getApplicationList()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -319,7 +263,7 @@ onMounted(() => {
top: 49px; top: 49px;
right: 16px; right: 16px;
z-index: 99; z-index: 99;
width: 400px; width: 600px;
box-shadow: 0px 4px 8px 0px var(--app-text-color-light-1); box-shadow: 0px 4px 8px 0px var(--app-text-color-light-1);
padding-bottom: 8px; padding-bottom: 8px;
@ -333,44 +277,18 @@ onMounted(() => {
} }
.list-item { .list-item {
box-sizing: border-box;
&:hover { &:hover {
border-color: var(--el-color-primary); border-color: var(--el-color-primary);
} }
} }
:deep(.el-collapse) { :deep(.el-tabs__header) {
border-top-width: 0; margin-bottom: 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 { :deep(.tree-height) {
background: transparent; height: 400px;
}
:deep(.el-tree-node__content) {
height: auto;
align-items: baseline;
&:hover {
background: transparent;
}
}
:deep(.tree-node--hidden) {
display: none !important;
} }
} }
</style> </style>

View File

@ -1,97 +1,78 @@
<template> <template>
<div class="w-full"> <el-input v-model.trim="filterText" :placeholder="$t('common.search')" prefix-icon="Search" clearable
<div v-if="data" class="flex align-center"> style="padding: 12px 12px 0 12px;" />
<AppIcon iconName="app-folder" style="font-size: 20px"></AppIcon> <div class="list flex-wrap">
<span <template v-if="filterList.length">
class="ml-8 ellipsis color-text-primary lighter" <el-popover v-for="item in filterList" :key="item.id" placement="right" :width="280" :show-after="500">
style="max-width: 110px" <template #reference>
:title="data.name" <div class="list-item flex align-center border border-r-6 p-8-12 cursor" style="width: calc(50% - 6px)"
> @click.stop="emit('clickNodes', item)" @mousedown.stop="emit('onmousedown', item)">
{{ data.name }} <LogoIcon v-if="item.resource_type === 'application'" height="32px" />
</span> <el-avatar v-else-if="isAppIcon(item?.icon)" shape="square" :size="32" style="background: none">
</div> <img :src="resetUrl(item?.icon)" alt="" />
</el-avatar>
<el-avatar v-else class="avatar-green" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" />
</el-avatar>
<span class="ml-8 ellipsis" :title="item.name">{{ item.name }}</span>
</div>
</template>
<transition name="el-fade-in-linear"> <template #default>
<div <div class="flex-between">
v-if="props.list?.length || (props.node?.expanded && toolList.length)" <div class="flex align-center">
class="list border-r-4 layout-bg flex-wrap"
@click.stop
>
<el-popover v-for="item in toolList" :key="item.id" placement="right" :width="280">
<template #reference>
<div
class="list-item flex align-center border border-r-6 p-8-12 cursor"
style="width: 39%"
@click.stop="emit('clickNodes', item)"
@mousedown.stop="emit('onmousedown', item)"
>
<LogoIcon v-if="item.resource_type === 'application'" height="32px" /> <LogoIcon v-if="item.resource_type === 'application'" height="32px" />
<el-avatar <el-avatar v-else-if="isAppIcon(item?.icon)" shape="square" :size="32" style="background: none">
v-else-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="resetUrl(item?.icon)" alt="" /> <img :src="resetUrl(item?.icon)" alt="" />
</el-avatar> </el-avatar>
<el-avatar v-else class="avatar-green" shape="square" :size="32"> <el-avatar v-else class="avatar-green" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" /> <img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" />
</el-avatar> </el-avatar>
<span class="ml-8 ellipsis" :title="item.name">{{ item.name }}</span> <span class="font-medium ml-8 break-all" :title="item.name">{{ item.name }}</span>
</div> </div>
</template> <div v-if="item.type" class="status-tag" style="margin-left: auto">
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
<template #default> {{ $t('views.application.workflow') }}
<div class="flex-between"> </el-tag>
<div class="flex align-center"> <el-tag class="blue-tag" v-else style="height: 22px">
<LogoIcon v-if="item.resource_type === 'application'" height="32px" /> {{ $t('views.application.simple') }}
<el-avatar </el-tag>
v-else-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="resetUrl(item?.icon)" alt="" />
</el-avatar>
<el-avatar v-else class="avatar-green" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" />
</el-avatar>
<span class="medium ml-8 ellipsis" :title="item.name">{{ item.name }}</span>
</div>
<div v-if="item.type" class="status-tag" style="margin-left: auto">
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
{{ $t('views.application.workflow') }}
</el-tag>
<el-tag class="blue-tag" v-else style="height: 22px">
{{ $t('views.application.simple') }}
</el-tag>
</div>
</div> </div>
<el-text type="info" size="small" class="mt-4">{{ item.desc }}</el-text> </div>
</template> <el-text type="info" size="small" class="mt-4">{{ item.desc }}</el-text>
</el-popover> </template>
</div> </el-popover>
</transition> </template>
<el-empty v-else :description="$t('common.noData')" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { watch, ref } from 'vue'
import { isAppIcon, resetUrl } from '@/utils/common' import { isAppIcon, resetUrl } from '@/utils/common'
import { isWorkFlow } from '@/utils/application' import { isWorkFlow } from '@/utils/application'
const props = defineProps<{ const props = defineProps<{
data?: any list: any[]
node?: any
list?: any[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'clickNodes', item: any): void (e: 'clickNodes', item: any): void;
(e: 'onmousedown', item: any): void (e: 'onmousedown', item: any): void;
}>() }>();
const toolList = computed(() => props.list ?? props.data?.cardList ?? []) const filterText = ref('')
const filterList = ref<any[]>([])
function filter(list: any[], filterText: string) {
if (!filterText.length) {
return list
}
return list.filter((v: any) => v.name.toLowerCase().includes(filterText.toLowerCase()))
}
watch([() => filterText.value, () => props.list], () => {
filterList.value = filter(props.list, filterText.value)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -99,15 +80,19 @@ const toolList = computed(() => props.list ?? props.data?.cardList ?? [])
cursor: default; cursor: default;
padding: 12px; padding: 12px;
gap: 12px; gap: 12px;
margin-top: 12px; box-sizing: border-box;
transform: translate(-16px, 0);
.list-item { .list-item {
background-color: #ffffff; background-color: #ffffff;
box-sizing: border-box;
&:hover { &:hover {
border-color: var(--el-color-primary); border-color: var(--el-color-primary);
} }
} }
.el-empty {
margin: 0 auto;
}
} }
</style> </style>