feat: 将工作流分享改为独立页面

- 新增无后台布局的工作流分享与失效路由

- 同步分享地址并保留旧链接兼容

- 补充前后端分享链路测试
This commit is contained in:
2026-07-24 18:51:53 +08:00
parent 526e16163b
commit 63eb55e24c
11 changed files with 212 additions and 25 deletions

View File

@@ -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;

View File

@@ -865,7 +865,7 @@ async function shareWorkflow(row: any) {
}
const shareUrl = buildAbsoluteAppRouteUrl(
router.resolve({
name: 'WorkflowDesign',
name: 'WorkflowShare',
query: {
shareKey: res.data.shareKey,
},

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
import WorkflowDesign from './WorkflowDesign.vue';
</script>
<template>
<WorkflowDesign share-mode />
</template>

View File

@@ -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');
});
});