feat: add template store functionality and appstore integration

This commit is contained in:
CaptainB 2025-12-25 18:16:55 +08:00
parent 9ed49599cf
commit 37ef7322cd
10 changed files with 741 additions and 28 deletions

View File

@ -12,9 +12,12 @@ import json
import os
import pickle
import re
import tempfile
import zipfile
from functools import reduce
from typing import Dict, List
import requests
import uuid_utils.compat as uuid
from django.core import validators
from django.db import models, transaction
@ -37,7 +40,9 @@ from common.database_model_manage.database_model_manage import DatabaseModelMana
from common.db.search import native_search, native_page_search
from common.exception.app_exception import AppApiException
from common.field.common import UploadedFileField
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines, \
bytes_to_uploaded_file
from common.utils.logger import maxkb_logger
from knowledge.models import Knowledge, KnowledgeScope
from knowledge.serializers.knowledge import KnowledgeSerializer, KnowledgeModelSerializer
from maxkb.conf import PROJECT_DIR
@ -182,28 +187,29 @@ class ApplicationCreateSerializer(serializers.Serializer):
node.get('properties')['node_data']['desc'] = application.get('desc')
node.get('properties')['node_data']['name'] = application.get('name')
node.get('properties')['node_data']['prologue'] = application.get('prologue')
return Application(id=uuid.uuid7(),
name=application.get('name'),
desc=application.get('desc'),
workspace_id=workspace_id,
folder_id=application.get('folder_id', application.get('workspace_id')),
prologue="",
dialogue_number=0,
user_id=user_id, model_id=None,
knowledge_setting={},
model_setting={},
problem_optimization=False,
type=ApplicationTypeChoices.WORK_FLOW,
stt_model_enable=application.get('stt_model_enable', False),
stt_model_id=application.get('stt_model', None),
tts_model_id=application.get('tts_model', None),
tts_model_enable=application.get('tts_model_enable', False),
tts_model_params_setting=application.get('tts_model_params_setting', {}),
tts_type=application.get('tts_type', 'BROWSER'),
file_upload_enable=application.get('file_upload_enable', False),
file_upload_setting=application.get('file_upload_setting', {}),
work_flow=default_workflow
)
return Application(
id=uuid.uuid7(),
name=application.get('name'),
desc=application.get('desc'),
workspace_id=workspace_id,
folder_id=application.get('folder_id', application.get('workspace_id')),
prologue="",
dialogue_number=0,
user_id=user_id, model_id=None,
knowledge_setting={},
model_setting={},
problem_optimization=False,
type=ApplicationTypeChoices.WORK_FLOW,
stt_model_enable=application.get('stt_model_enable', False),
stt_model_id=application.get('stt_model', None),
tts_model_id=application.get('tts_model', None),
tts_model_enable=application.get('tts_model_enable', False),
tts_model_params_setting=application.get('tts_model_params_setting', {}),
tts_type=application.get('tts_type', 'BROWSER'),
file_upload_enable=application.get('file_upload_enable', False),
file_upload_setting=application.get('file_upload_setting', {}),
work_flow=default_workflow
)
class SimplateRequest(serializers.Serializer):
name = serializers.CharField(required=True, max_length=64, min_length=1,
@ -459,8 +465,14 @@ class ApplicationSerializer(serializers.Serializer):
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
user_id = serializers.UUIDField(required=True, label=_("User ID"))
@transaction.atomic
def insert(self, instance: Dict):
work_flow_template = instance.get('work_flow_template')
application_type = instance.get('type')
# 处理工作流模板安装逻辑
if work_flow_template:
return self.insert_template_workflow(instance)
if 'WORK_FLOW' == application_type:
r = self.insert_workflow(instance)
else:
@ -472,6 +484,35 @@ class ApplicationSerializer(serializers.Serializer):
}).auth_resource(str(r.get('id')))
return r
def insert_template_workflow(self, instance: Dict):
self.is_valid(raise_exception=True)
work_flow_template = instance.get('work_flow_template')
download_url = work_flow_template.get('downloadUrl')
# 查找匹配的版本名称
res = requests.get(download_url, timeout=5)
app = ApplicationSerializer(
data={'user_id': self.data.get('user_id'), 'workspace_id': self.data.get('workspace_id')}
).import_({
'file': bytes_to_uploaded_file(res.content, 'file.mk'),
'folder_id': instance.get('folder_id', instance.get('workspace_id'))
}, True)
work_flow = app.get('work_flow')
for node in work_flow.get('nodes', []):
if node.get('type') == 'base-node':
node_data = node.get('properties').get('node_data')
node_data['name'] = instance.get('name')
node_data['desc'] = instance.get('desc')
QuerySet(Application).filter(id=app.get('id')).update(
name=instance.get('name'),
desc=instance.get('desc'),
work_flow=work_flow
)
try:
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
except Exception as e:
maxkb_logger.error(f"callback appstore tool download error: {e}")
return app
def insert_workflow(self, instance: Dict):
self.is_valid(raise_exception=True)
user_id = self.data.get('user_id')
@ -565,7 +606,8 @@ class ApplicationSerializer(serializers.Serializer):
'user_id': self.data.get('user_id'),
'auth_target_type': AuthTargetType.TOOL.value
}).auth_resource_batch([t.id for t in tool_model_list])
return True
return ApplicationCreateSerializer.ApplicationResponse(application_model).data
@staticmethod
def to_tool(tool, workspace_id, user_id):
@ -620,6 +662,55 @@ class ApplicationSerializer(serializers.Serializer):
file_upload_setting=application.get('file_upload_setting'),
)
class StoreApplication(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_("User ID"))
name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
def get_appstore_templates(self):
self.is_valid(raise_exception=True)
# 下载zip文件
try:
res = requests.get('https://apps-assets.fit2cloud.com/stable/maxkb.json.zip', timeout=5)
res.raise_for_status()
# 创建临时文件保存zip
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as temp_zip:
temp_zip.write(res.content)
temp_zip_path = temp_zip.name
try:
# 解压zip文件
with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
# 获取zip中的第一个文件假设只有一个json文件
json_filename = zip_ref.namelist()[0]
json_content = zip_ref.read(json_filename)
# 将json转换为字典
tool_store = json.loads(json_content.decode('utf-8'))
tag_dict = {tag['name']: tag['key'] for tag in tool_store['additionalProperties']['tags']}
filter_apps = []
for tool in tool_store['apps']:
if self.data.get('name', '') != '':
if self.data.get('name').lower() not in tool.get('name', '').lower():
continue
if not tool['downloadUrl'].endswith('.mk'):
continue
versions = tool.get('versions', [])
tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else ''
tool['version'] = next(
(version.get('name') for version in versions if
version.get('downloadUrl') == tool['downloadUrl']),
)
filter_apps.append(tool)
tool_store['apps'] = filter_apps
return tool_store
finally:
# 清理临时文件
os.unlink(temp_zip_path)
except Exception as e:
maxkb_logger.error(f"fetch appstore tools error: {e}")
return {'apps': [], 'additionalProperties': {'tags': []}}
class TextToSpeechRequest(serializers.Serializer):
text = serializers.CharField(required=True, label=_('Text'))
@ -772,7 +863,7 @@ class ApplicationOperateSerializer(serializers.Serializer):
work_flow_version.save()
access_token = hashlib.md5(
str(uuid.uuid7()).encode()).hexdigest()[
8:24]
8:24]
application_access_token = QuerySet(ApplicationAccessToken).filter(
application_id=application.id).first()
if application_access_token is None:
@ -828,6 +919,10 @@ class ApplicationOperateSerializer(serializers.Serializer):
application_id = self.data.get("application_id")
application = QuerySet(Application).get(id=application_id)
# 处理工作流模板逻辑
if 'work_flow_template' in instance:
return self.update_template_workflow(instance, application)
if instance.get('model_id') is None or len(instance.get('model_id')) == 0:
application.model_id = None
else:
@ -880,6 +975,80 @@ class ApplicationOperateSerializer(serializers.Serializer):
self.save_application_knowledge_mapping(application_knowledge_id_list, knowledge_id_list, application_id)
return self.one(with_valid=False)
def update_template_workflow(self, instance: Dict, app: Application):
self.is_valid(raise_exception=True)
work_flow_template = instance.get('work_flow_template')
download_url = work_flow_template.get('downloadUrl')
# 查找匹配的版本名称
res = requests.get(download_url, timeout=5)
try:
mk_instance = restricted_loads(res.content)
except Exception as e:
raise AppApiException(1001, _("Unsupported file format"))
application = mk_instance.application
tool_list = mk_instance.get_tool_list()
update_tool_map = {}
if len(tool_list) > 0:
tool_id_list = reduce(lambda x, y: [*x, *y],
[[tool.get('id'), generate_uuid((tool.get('id') + app.workspace_id or ''))]
for tool
in
tool_list], [])
# 存在的工具列表
exits_tool_id_list = [str(tool.id) for tool in
QuerySet(Tool).filter(id__in=tool_id_list, workspace_id=app.workspace_id)]
# 需要更新的工具集合
update_tool_map = {tool.get('id'): generate_uuid((tool.get('id') + app.workspace_id or '')) for tool
in
tool_list if
not exits_tool_id_list.__contains__(
tool.get('id'))}
tool_list = [{**tool, 'id': update_tool_map.get(tool.get('id'))} for tool in tool_list if
not exits_tool_id_list.__contains__(
tool.get('id')) and not exits_tool_id_list.__contains__(
generate_uuid((tool.get('id') + app.workspace_id or '')))]
tool_model_list = [self.to_tool(f, app.workspace_id, self.data.get('user_id')) for f in tool_list]
work_flow = application.get('work_flow')
for node in work_flow.get('nodes', []):
hand_node(node, update_tool_map)
if node.get('type') == 'loop-node':
for n in node.get('properties', {}).get('node_data', {}).get('loop_body', {}).get('nodes', []):
hand_node(n, update_tool_map)
app.work_flow = work_flow
app.save()
if len(tool_model_list) > 0:
QuerySet(Tool).bulk_create(tool_model_list)
UserResourcePermissionSerializer(data={
'workspace_id': app.workspace_id,
'user_id': self.data.get('user_id'),
'auth_target_type': AuthTargetType.TOOL.value
}).auth_resource_batch([t.id for t in tool_model_list])
try:
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
except Exception as e:
maxkb_logger.error(f"callback appstore tool download error: {e}")
return self.one(with_valid=False)
@staticmethod
def to_tool(tool, workspace_id, user_id):
return Tool(
id=tool.get('id'),
user_id=user_id,
name=tool.get('name'),
code=tool.get('code'),
template_id=tool.get('template_id'),
input_field_list=tool.get('input_field_list'),
init_field_list=tool.get('init_field_list'),
is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'),
scope=ToolScope.WORKSPACE,
folder_id=workspace_id,
workspace_id=workspace_id
)
def one(self, with_valid=True):
if with_valid:
self.is_valid()

View File

@ -5,7 +5,7 @@ from . import views
app_name = 'application'
# @formatter:off
urlpatterns = [
path('workspace/store/application_template', views.ApplicationAPI.StoreApplication.as_view()),
path('workspace/<str:workspace_id>/application', views.ApplicationAPI.as_view(), name='application'),
path('workspace/<str:workspace_id>/application/folder/<str:folder_id>/import', views.ApplicationAPI.Import.as_view()),
path('workspace/<str:workspace_id>/application/<int:current_page>/<int:page_size>', views.ApplicationAPI.Page.as_view(), name='application_page'),

View File

@ -23,6 +23,7 @@ from common.auth import TokenAuth
from common.auth.authentication import has_permissions, get_is_permissions
from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
from common.log.log import log
from tools.api.tool import GetInternalToolAPI
def get_application_operation_object(application_id):
@ -251,6 +252,23 @@ class ApplicationAPI(APIView):
data={'application_id': application_id, 'user_id': request.user.id,
'workspace_id': workspace_id, }).publish(request.data))
class StoreApplication(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
methods=['GET'],
description=_("Get Appstore apps"),
summary=_("Get Appstore apps"),
operation_id=_("Get Appstore apps"), # type: ignore
responses=GetInternalToolAPI.get_response(),
tags=[_("Application")] # type: ignore
)
def get(self, request: Request):
return result.success(ApplicationSerializer.StoreApplication(data={
'user_id': request.user.id,
'name': request.query_params.get('name', ''),
}).get_appstore_templates())
class McpServers(APIView):
authentication_classes = [TokenAuth]

View File

@ -37,6 +37,7 @@ interface ApplicationFormType {
application_enable?: boolean
application_ids?: string[]
mcp_output_enable?: boolean
work_flow_template?: any
}
interface Chunk {

View File

@ -26,6 +26,14 @@
</el-button>
</div>
<div v-else>
<el-button
class="ml-8"
v-if="permissionPrecise.edit(id)"
@click="openTemplateStoreDialog()"
>
<AppIcon iconName="app-template-center" class="mr-4" />
{{ $t('workflow.setting.templateCenter') }}
</el-button>
<el-button @click="showPopover = !showPopover">
<AppIcon iconName="app-add-outlined" class="mr-4" />
{{ $t('workflow.setting.addComponent') }}
@ -137,6 +145,12 @@
v-click-outside="clickoutsideHistory"
@refreshVersion="refreshVersion"
/>
<TemplateStoreDialog
ref="templateStoreDialogRef"
:api-type="apiType"
source="work_flow"
@refresh="getDetail"
/>
</div>
</template>
<script setup lang="ts">
@ -159,6 +173,7 @@ import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/dat
import permissionMap from '@/permission'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { WorkflowMode } from '@/enums/application'
import TemplateStoreDialog from "@/views/application/template-store/TemplateStoreDialog.vue";
provide('getResourceDetail', () => detail)
provide('workflowMode', WorkflowMode.Application)
provide('loopWorkflowMode', WorkflowMode.ApplicationLoop)
@ -635,6 +650,11 @@ const closeInterval = () => {
}
}
const templateStoreDialogRef = ref()
function openTemplateStoreDialog() {
templateStoreDialogRef.value?.open()
}
onMounted(() => {
getDetail()
const workflowAutoSave = localStorage.getItem('workflowAutoSave')

View File

@ -41,7 +41,7 @@
<el-form-item
:label="$t('views.document.upload.template')"
v-if="applicationForm.type === 'WORK_FLOW'"
v-if="applicationForm.type === 'WORK_FLOW' && !work_flow_template"
>
<div class="w-full">
<el-row :gutter="16">
@ -126,6 +126,7 @@ const applicationFormRef = ref()
const loading = ref(false)
const dialogVisible = ref<boolean>(false)
const work_flow_template = ref()
const applicationForm = ref<ApplicationFormType>({
name: '',
@ -158,6 +159,7 @@ const applicationForm = ref<ApplicationFormType>({
tts_model_enable: false,
tts_type: 'BROWSER',
type: 'SIMPLE',
work_flow_template: undefined,
})
const rules = reactive<FormRules<ApplicationFormType>>({
@ -217,10 +219,11 @@ watch(dialogVisible, (bool) => {
}
})
const open = (folder: string, type?: string) => {
const open = (folder: string, type?: string, work_flow?: any) => {
currentFolder.value = folder
applicationForm.value.type = type || 'SIMPLE'
dialogVisible.value = true
work_flow_template.value = work_flow
}
const submitHandle = async (formEl: FormInstance | undefined) => {
@ -231,6 +234,9 @@ const submitHandle = async (formEl: FormInstance | undefined) => {
workflowDefault.value.nodes[0].properties.node_data.desc = applicationForm.value.desc
workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name
applicationForm.value['work_flow'] = workflowDefault.value
if (work_flow_template.value) {
applicationForm.value['work_flow_template'] = work_flow_template.value
}
}
loading.value = true
applicationApi

View File

@ -61,6 +61,14 @@
<el-option :label="$t('common.status.unpublished')" value="unpublished" />
</el-select>
</div>
<el-button
class="ml-8"
v-if="permissionPrecise.create()"
@click="openTemplateStoreDialog()"
>
<AppIcon iconName="app-template-center" class="mr-4" />
{{ $t('workflow.setting.templateCenter') }}
</el-button>
<el-dropdown trigger="click" v-if="permissionPrecise.create()">
<el-button type="primary" class="ml-8">
{{ $t('common.create') }}
@ -291,6 +299,7 @@
:type="SourceTypeEnum.APPLICATION"
ref="ResourceAuthorizationDrawerRef"
/>
<TemplateStoreDialog ref="templateStoreDialogRef" :api-type="apiType" @refresh="getList" />
</LayoutContainer>
</template>
@ -316,6 +325,7 @@ import WorkspaceApi from '@/api/workspace/workspace'
import { hasPermission } from '@/utils/permission'
import { ComplexPermission } from '@/utils/permission/type'
import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/data'
import TemplateStoreDialog from "@/views/application/template-store/TemplateStoreDialog.vue";
const router = useRouter()
@ -685,6 +695,11 @@ function searchHandle() {
getList()
}
const templateStoreDialogRef = ref()
function openTemplateStoreDialog() {
templateStoreDialogRef.value?.open(folder.currentFolder.id)
}
function getList() {
const params: any = {
folder_id: folder.currentFolder?.id || 'default',

View File

@ -0,0 +1,86 @@
<template>
<el-drawer v-model="visibleInternalDesc" size="60%" :append-to-body="true">
<template #header>
<div class="flex align-center" style="margin-left: -8px">
<el-button class="cursor mr-4" link @click.prevent="visibleInternalDesc = false">
<el-icon :size="20">
<Back />
</el-icon>
</el-button>
<h4>{{ $t('common.detail') }}</h4>
</div>
</template>
<div>
<div class="border-b">
<div class="flex-between mb-24">
<div class="title flex align-center">
<el-avatar shape="square" :size="64" style="background: none">
<img src="@/assets/knowledge/icon_basic_template.svg" alt="" />
</el-avatar>
<div class="ml-16">
<h3 class="mb-8">{{ toolDetail.name }}</h3>
<el-text type="info" v-if="toolDetail?.desc">
{{ toolDetail.desc }}
</el-text>
<span
class="color-secondary flex align-center mt-8"
v-if="toolDetail?.downloads != undefined"
>
<AppIcon iconName="app-download" class="mr-4" />
<span> {{ numberFormat(toolDetail.downloads || 0) }} </span>
</span>
</div>
</div>
<div @click.stop>
<el-button type="primary" @click="addInternalTool(toolDetail)">
{{ $t('common.use') }}
</el-button>
</div>
</div>
</div>
<MdPreview
ref="editorRef"
editorId="preview-only"
:modelValue="markdownContent"
style="background: none"
noImgZoomIn
/>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import { isAppIcon, numberFormat } from '@/utils/common'
const emit = defineEmits(['refresh', 'addTool'])
const visibleInternalDesc = ref(false)
const markdownContent = ref('')
const toolDetail = ref<any>({})
watch(visibleInternalDesc, (bool) => {
if (!bool) {
markdownContent.value = ''
}
})
const open = (data: any, detail: any) => {
toolDetail.value = detail
if (data) {
markdownContent.value = cloneDeep(data)
}
visibleInternalDesc.value = true
}
const addInternalTool = (data: any) => {
emit('addTool', data)
visibleInternalDesc.value = false
}
defineExpose({
open,
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,83 @@
<template>
<CardBox :title="props.tool.name" :description="props.tool.desc" class="cursor tool-card">
<template #icon>
<el-avatar shape="square" :size="32" style="background: none">
<img src="@/assets/knowledge/icon_basic_template.svg" alt="" />
</el-avatar>
</template>
<template #title>
<span :title="props.tool?.name" class="ellipsis"> {{ props.tool?.name }}</span>
</template>
<!-- <template #tag>
<el-tag type="info" v-if="props.tool?.label === 'knowledge_template'" class="info-tag">
{{ $t('知识库') }}
</el-tag>
<el-tag type="info" class="info-tag" v-else>
{{ $t('views.tool.title') }}
</el-tag>
</template> -->
<!-- <template #subTitle>
<el-text class="color-secondary lighter" size="small">
{{ getSubTitle(props.tool) }}
</el-text>
</template> -->
<template #footer>
<span class="card-footer-left color-secondary flex align-center" v-if="props.tool?.downloads != undefined">
<AppIcon iconName="app-download" class="mr-4" />
<span> {{ numberFormat(props.tool.downloads || 0) }} </span>
</span>
<div class="card-footer-operation mb-8" @click.stop>
<el-button @click="emit('handleDetail')">
{{ $t('common.detail') }}
</el-button>
<el-button type="primary" :loading="props.addLoading" @click="emit('handleAdd')">
{{ $t('common.use') }}
</el-button>
</div>
</template>
</CardBox>
</template>
<script setup lang="ts">
import { isAppIcon, numberFormat, resetUrl } from '@/utils/common'
const props = defineProps<{
tool: any
getSubTitle: (v: any) => string
addLoading: boolean
}>()
const emit = defineEmits<{
(e: 'handleAdd'): void
(e: 'handleDetail'): void
}>()
</script>
<style lang="scss" scoped>
.tool-card {
:deep(.card-footer) {
& > div:first-of-type {
flex: 1;
}
.card-footer-operation {
display: none;
}
}
&:hover {
.card-footer-left {
display: none;
}
.card-footer-operation {
display: flex !important;
.el-button {
flex: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,315 @@
<template>
<el-dialog
v-model="dialogVisible"
width="1000"
append-to-body
class="tool-store-dialog"
align-center
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId }">
<div class="dialog-header flex-between mb-8">
<h4 :id="titleId" class="medium w-240 mr-8">
{{ $t('workflow.setting.templateCenter') }}
</h4>
<div class="flex align-center" style="margin-right: 28px">
<el-input
v-model="searchValue"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="w-240 mr-8"
clearable
@change="getList"
/>
<el-divider direction="vertical" />
</div>
</div>
</template>
<!-- <LayoutContainer v-loading="loading" :minLeftWidth="204">
<template #left>
<el-anchor
direction="vertical"
:offset="130"
type="default"
container=".category-scrollbar"
@click="handleClick"
>
<el-anchor-link
v-for="category in categories"
:key="category.id"
:href="`#category-${category.id}`"
:title="category.title"
/>
</el-anchor>
</template> -->
<el-scrollbar class="layout-bg" wrap-class="p-16-24 category-scrollbar">
<template v-if="filterList === null">
<div v-for="category in categories" :key="category.id">
<!-- <h4
class="title-decoration-1 mb-16 mt-8 color-text-primary"
:id="`category-${category.id}`"
>
{{ category.title }}
</h4> -->
<el-row :gutter="16">
<el-col v-for="tool in category.tools" :key="tool.id" :span="8" class="mb-16">
<TemplateCard
:tool="tool"
:addLoading="addLoading"
:get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)"
@handleDetail="handleDetail(tool)"
>
</TemplateCard>
</el-col>
</el-row>
</div>
</template>
<div v-else>
<!-- <h4 class="color-text-primary medium mb-16">
<span class="color-primary">{{ searchValue }}</span>
{{ t('views.tool.toolStore.searchResult', { count: filterList.length }) }}
</h4> -->
<el-row :gutter="16" v-if="filterList.length">
<el-col v-for="tool in filterList" :key="tool.id" :span="12" class="mb-16">
<TemplateCard
:tool="tool"
:addLoading="addLoading"
:get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)"
@handleDetail="handleDetail(tool)"
/>
</el-col>
</el-row>
<el-empty v-else :description="$t('common.noData')" />
</div>
</el-scrollbar>
<!-- </LayoutContainer> -->
</el-dialog>
<InternalDescDrawer ref="internalDescDrawerRef" @addTool="handleOpenAdd" />
<CreateApplicationDialog ref="CreateKnowledgeDialogRef" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ToolStoreApi from '@/api/tool/store'
import { t } from '@/locales'
import TemplateCard from './TemplateCard.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import InternalDescDrawer from './InternalDescDrawer.vue'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api.ts'
import useStore from '@/stores'
import { useRoute } from 'vue-router'
import CreateApplicationDialog from '@/views/application/component/CreateApplicationDialog.vue'
const { user } = useStore()
const route = useRoute()
const {
params: { id },
/*
folderId 可以区分 resource-management shared还是 workspace
*/
} = route as any
interface ToolCategory {
id: string
title: string
tools: any[]
}
const props = defineProps({
apiType: {
type: String as () => 'workspace' | 'systemShare' | 'systemManage' | 'workspaceShare',
default: 'workspace',
},
source: {
type: String,
default: 'application',
},
})
const emit = defineEmits(['refresh'])
const dialogVisible = ref(false)
const loading = ref(false)
const searchValue = ref('')
const folderId = ref('')
const categories = ref<ToolCategory[]>([])
const filterList = ref<any>(null)
function getSubTitle(tool: any) {
return categories.value.find((i) => i.id === tool.label)?.title ?? ''
}
function open(id: string) {
folderId.value = id
filterList.value = null
dialogVisible.value = true
getList()
}
async function getList() {
filterList.value = null
const [v1] = await Promise.all([getStoreToolList()])
const merged = [...v1].reduce((acc, category) => {
const existing = acc.find((item: any) => item.id === category.id)
if (existing) {
existing.tools = [...existing.tools, ...category.tools]
} else {
acc.push({ ...category })
}
return acc
}, [] as ToolCategory[])
categories.value = merged.filter((item: any) => item.tools.length > 0)
}
async function getStoreToolList() {
try {
const res = await ToolStoreApi.getStoreAppList({ name: searchValue.value }, loading)
const tags = res.data.additionalProperties.tags
const storeTools = res.data.apps
let categories = []
//
storeTools.forEach((tool: any) => {
tool.desc = tool.description
})
if (searchValue.value.length) {
filterList.value = [...res.data.apps, ...(filterList.value || [])]
} else {
filterList.value = null
categories = tags.map((tag: any) => ({
id: tag.key,
title: tag.name, //
tools: storeTools.filter((tool: any) => tool.label === tag.key),
}))
}
return categories
} catch (error) {
console.error(error)
return []
}
}
const handleClick = (e: MouseEvent) => {
e.preventDefault()
}
const internalDescDrawerRef = ref<InstanceType<typeof InternalDescDrawer>>()
async function handleDetail(tool: any) {
internalDescDrawerRef.value?.open(tool.readMe, tool)
}
const CreateKnowledgeDialogRef = ref()
function handleOpenAdd(data?: any, isEdit?: boolean) {
if (props.source === 'work_flow') {
MsgConfirm(
t('common.tip'),
`${t('views.application.tip.confirmUse')} ${data.name} ${t('views.application.tip.overwrite')}?`,
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
},
)
.then(() => {
handleStoreAdd(data)
})
.catch(() => {})
} else {
CreateKnowledgeDialogRef.value.open(folderId.value, 'WORK_FLOW', data)
}
}
const addLoading = ref(false)
function handleStoreAdd(tool: any) {
try {
loadSharedApi({ type: 'application', systemType: props.apiType })
.putApplication(id, { work_flow_template: tool }, addLoading)
.then(() => {
emit('refresh')
MsgSuccess(t('common.addSuccess'))
})
dialogVisible.value = false
} catch (error) {
console.error(error)
}
}
defineExpose({ open })
</script>
<style lang="scss">
.tool-store-dialog {
padding: 0;
.el-dialog__headerbtn {
top: 7px;
}
.el-dialog__header {
padding: 12px 20px 4px 24px;
border-bottom: 1px solid var(--el-border-color-light);
.dialog-header {
position: relative;
.store-type {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.layout-container__left {
background-color: var(--app-layout-bg-color);
border-radius: 0 0 0 8px;
}
.layout-container__right {
background-color: var(--app-layout-bg-color);
border-radius: 0 0 8px 0;
}
.el-anchor {
background-color: var(--app-layout-bg-color);
.el-anchor__marker {
display: none;
}
.el-anchor__list {
padding: 8px;
}
.el-anchor__item {
.el-anchor__link {
padding: 8px 16px;
font-weight: 500;
font-size: 14px;
color: var(--el-text-color-primary);
border-radius: 6px;
&.is-active {
color: var(--el-color-primary);
background-color: #3370ff1a;
}
}
}
}
.category-scrollbar {
height: calc(100vh - 200px);
// min-height: 500px;
}
}
</style>