feat: 将工作流分享改为独立页面
- 新增无后台布局的工作流分享与失效路由 - 同步分享地址并保留旧链接兼容 - 补充前后端分享链路测试
This commit is contained in:
@@ -20,7 +20,10 @@ import {api} from '#/api/request';
|
||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||
import {$t} from '#/locales';
|
||||
import {router} from '#/router';
|
||||
import { resolveWorkflowShareWorkflowId } from '#/utils/workflow-share-context';
|
||||
import {
|
||||
resolveWorkflowShareFailureReason,
|
||||
resolveWorkflowShareWorkflowId,
|
||||
} from '#/utils/workflow-share-context';
|
||||
import {getIconByValue} from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import {
|
||||
canAiResourceRepublish,
|
||||
@@ -49,6 +52,14 @@ import {
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
shareMode?: boolean;
|
||||
}>(),
|
||||
{
|
||||
shareMode: false,
|
||||
},
|
||||
);
|
||||
const route = useRoute();
|
||||
const { isDark } = usePreferences();
|
||||
// vue
|
||||
@@ -86,16 +97,29 @@ const codeEngineList = ref<any[]>([
|
||||
]);
|
||||
|
||||
async function resolveSharedWorkflowId() {
|
||||
const shareKey = Array.isArray(route.query.shareKey)
|
||||
? route.query.shareKey.find((value) => String(value || '').trim())
|
||||
: route.query.shareKey;
|
||||
if (props.shareMode && !String(shareKey || '').trim()) {
|
||||
await router.replace({
|
||||
name: 'WorkflowShareExpired',
|
||||
query: { reason: 'invalid' },
|
||||
});
|
||||
workflowId.value = null;
|
||||
return;
|
||||
}
|
||||
workflowId.value = await resolveWorkflowShareWorkflowId({
|
||||
currentWorkflowId: workflowId.value,
|
||||
shareKey: route.query.shareKey,
|
||||
shareKey,
|
||||
resolve: async () => {
|
||||
const res = await api.get('/api/v1/workflowShare/resolve');
|
||||
return res.data?.workflowId;
|
||||
},
|
||||
onFailure: async () => {
|
||||
ElMessage.error($t('aiWorkflow.shareExpired'));
|
||||
await router.replace({ path: '/ai/workflow' });
|
||||
onFailure: async (error) => {
|
||||
await router.replace({
|
||||
name: 'WorkflowShareExpired',
|
||||
query: { reason: resolveWorkflowShareFailureReason(error) },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -787,7 +811,10 @@ function onAsyncExecute(info: any) {
|
||||
<template>
|
||||
<div
|
||||
class="head-div h-full w-full"
|
||||
:class="{ 'workflow-issue-focus': issueFocusActive }"
|
||||
:class="{
|
||||
'head-div--share': props.shareMode,
|
||||
'workflow-issue-focus': issueFocusActive,
|
||||
}"
|
||||
v-loading="pageLoading"
|
||||
>
|
||||
<CommonSelectDataModal
|
||||
@@ -846,9 +873,16 @@ function onAsyncExecute(info: any) {
|
||||
:polling-data="chainInfo"
|
||||
/>
|
||||
</ElDrawer>
|
||||
<div class="flex items-center justify-between border-b p-2.5">
|
||||
<div>
|
||||
<ElButton :icon="ArrowLeft" link @click="router.back()">
|
||||
<div
|
||||
class="workflow-designer-header flex items-center justify-between gap-2 border-b p-2.5"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<ElButton
|
||||
v-if="!props.shareMode"
|
||||
:icon="ArrowLeft"
|
||||
link
|
||||
@click="router.back()"
|
||||
>
|
||||
<span
|
||||
class="max-w-[500px] overflow-hidden text-ellipsis text-nowrap text-base"
|
||||
style="font-size: 14px"
|
||||
@@ -857,6 +891,9 @@ function onAsyncExecute(info: any) {
|
||||
{{ workflowInfo.title }}
|
||||
</span>
|
||||
</ElButton>
|
||||
<div v-else class="workflow-share-title" :title="workflowInfo.title">
|
||||
{{ workflowInfo.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-head-actions">
|
||||
<ElButton
|
||||
@@ -893,7 +930,7 @@ function onAsyncExecute(info: any) {
|
||||
class="load-div"
|
||||
>
|
||||
<template #extra>
|
||||
<ElButton @click="backToWorkflowList">
|
||||
<ElButton v-if="!props.shareMode" @click="backToWorkflowList">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="initializeWorkflow">
|
||||
@@ -1001,6 +1038,40 @@ function onAsyncExecute(info: any) {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.head-div--share {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh !important;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.head-div--share :deep(.agentsflow) {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.head-div--share .tiny-flow-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.workflow-designer-header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.workflow-share-title {
|
||||
max-width: min(500px, 48vw);
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-head-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -865,7 +865,7 @@ async function shareWorkflow(row: any) {
|
||||
}
|
||||
const shareUrl = buildAbsoluteAppRouteUrl(
|
||||
router.resolve({
|
||||
name: 'WorkflowDesign',
|
||||
name: 'WorkflowShare',
|
||||
query: {
|
||||
shareKey: res.data.shareKey,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import WorkflowDesign from './WorkflowDesign.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkflowDesign share-mode />
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
isWorkflowShareRequest,
|
||||
readWorkflowShareKey,
|
||||
resolveWorkflowShareFailureReason,
|
||||
resolveWorkflowShareWorkflowId,
|
||||
withWorkflowShareHeader,
|
||||
WORKFLOW_SHARE_HEADER,
|
||||
@@ -25,6 +26,14 @@ describe('workflow share context', () => {
|
||||
).toBe('hash-key');
|
||||
});
|
||||
|
||||
it('reads the share key from the standalone workflow share route', () => {
|
||||
expect(
|
||||
readWorkflowShareKey(
|
||||
'https://example.test/flow/#/share/workflow?shareKey=standalone-key',
|
||||
),
|
||||
).toBe('standalone-key');
|
||||
});
|
||||
|
||||
it('adds the workflow share header without dropping existing headers', () => {
|
||||
expect(
|
||||
withWorkflowShareHeader(
|
||||
@@ -163,4 +172,15 @@ describe('workflow share context', () => {
|
||||
).resolves.toBeNull();
|
||||
expect(onFailure).toHaveBeenCalledWith(resolveError);
|
||||
});
|
||||
|
||||
it('maps workflow share failures to the expired page reason', () => {
|
||||
expect(
|
||||
resolveWorkflowShareFailureReason({
|
||||
response: { data: { message: '工作流分享链接已过期' } },
|
||||
}),
|
||||
).toBe('expired');
|
||||
expect(
|
||||
resolveWorkflowShareFailureReason(new Error('工作流分享链接无效')),
|
||||
).toBe('invalid');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user