Add workflow sharing functionality (#371)

* Add workflow sharing feature with share, fork, and browse functionality

Co-authored-by: jxxghp <jxxghp@163.com>

* Refactor workflow page with dynamic tabs and internationalization support

Co-authored-by: jxxghp <jxxghp@163.com>

* Remove workflow share implementation documentation

Co-authored-by: jxxghp <jxxghp@163.com>

* Fix indentation and structure in Chinese locale files

Co-authored-by: jxxghp <jxxghp@163.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
jxxghp
2025-07-08 23:31:22 +08:00
committed by GitHub
parent f2bc832aca
commit 35332544e4
14 changed files with 876 additions and 27 deletions

View File

@@ -0,0 +1,121 @@
<script lang="ts" setup>
import { formatDateDifference } from '@/@core/utils/formatters'
import type { WorkflowShare } from '@/api/types'
import WorkflowEditDialog from '../dialog/WorkflowAddEditDialog.vue'
import ForkWorkflowDialog from '../dialog/ForkWorkflowDialog.vue'
import { useGlobalSettingsStore } from '@/stores'
// 输入参数
const props = defineProps({
media: Object as PropType<WorkflowShare>,
})
// 定义删除事件
const emit = defineEmits(['delete'])
// 从 provide 中获取全局设置
// 全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 工作流编辑弹窗
const workflowEditDialog = ref(false)
// 复用工作流弹窗
const forkWorkflowDialog = ref(false)
// 工作流ID
const workflowId = ref<string>()
// 分享时间
const dateText = ref(props.media && props.media?.date ? formatDateDifference(props.media.date) : '')
// 复用工作流
function showForkWorkflow() {
forkWorkflowDialog.value = true
}
// 完成复用工作流
function finishForkWorkflow(wid: string) {
workflowId.value = wid
forkWorkflowDialog.value = false
workflowEditDialog.value = true
}
// 删除工作流分享时处理
function doDelete() {
forkWorkflowDialog.value = false
// 通知父组件刷新
emit('delete')
}
</script>
<template>
<div class="h-full">
<VHover>
<template #default="hover">
<div
class="w-full h-full rounded-lg overflow-hidden"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
>
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
rounded="0"
min-height="150"
@click="showForkWorkflow"
>
<div class="h-full flex flex-col">
<VCardText class="flex items-center pa-3 pb-1 grow">
<div class="flex flex-col justify-center">
<div class="mr-2 min-w-0 text-lg font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.share_title }}
</div>
<div class="text-sm font-medium text-gray-600 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
{{ props.media?.share_comment }}
</div>
</div>
</VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap py-2">
<div class="flex align-center">
<IconBtn v-bind="props" icon="mdi-account" class="me-1" />
<div class="text-subtitle-2 me-4">
{{ props.media?.share_user }}
</div>
<IconBtn v-if="props.media?.count" icon="mdi-fire" class="me-1" />
<span v-if="props.media?.count" class="text-subtitle-2 me-4">
{{ props.media?.count.toLocaleString() }}
</span>
</div>
</VCardText>
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-500">
<VIcon icon="mdi-calendar" class="me-1" />
{{ dateText }}
</VCardText>
</div>
</VCard>
</div>
</template>
</VHover>
<!-- 工作流编辑弹窗 -->
<WorkflowEditDialog
v-if="workflowEditDialog"
v-model="workflowEditDialog"
:workflow="workflowId ? { id: workflowId } : undefined"
@close="workflowEditDialog = false"
@save="workflowEditDialog = false"
@remove="workflowEditDialog = false"
/>
<!-- 复用工作流弹窗 -->
<ForkWorkflowDialog
v-if="forkWorkflowDialog"
:media="props.media"
@close="forkWorkflowDialog = false"
@fork="finishForkWorkflow"
@delete="doDelete"
/>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import { useToast } from 'vue-toastification'
import { useConfirm } from '@/composables/useConfirm'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
import WorkflowShareDialog from '@/components/dialog/WorkflowShareDialog.vue'
import api from '@/api'
import { useI18n } from 'vue-i18n'
@@ -32,6 +33,9 @@ const editDialog = ref(false)
// 流程对话框
const flowDialog = ref(false)
// 分享对话框
const shareDialog = ref(false)
// 加载中
const loading = ref(false)
@@ -45,10 +49,16 @@ function handleFlow(item: Workflow) {
flowDialog.value = true
}
// 分享工作流
function handleShare(item: Workflow) {
shareDialog.value = true
}
// 编辑完成
function editDone() {
editDialog.value = false
flowDialog.value = false
shareDialog.value = false
emit('refresh')
}
@@ -240,6 +250,12 @@ const resolveProgress = (item: Workflow) => {
</template>
<VListItemTitle>{{ t('workflow.task.reset') }}</VListItemTitle>
</VListItem>
<VListItem base-color="info" @click="handleShare(workflow)">
<template #prepend>
<VIcon icon="mdi-share" />
</template>
<VListItemTitle>{{ t('workflow.task.share') }}</VListItemTitle>
</VListItem>
<VListItem base-color="error" @click="handleDelete(workflow)">
<template #prepend>
<VIcon icon="mdi-delete" />
@@ -317,5 +333,11 @@ const resolveProgress = (item: Workflow) => {
@save="editDone"
:workflow="workflow"
/>
<!-- 分享对话框 -->
<WorkflowShareDialog
v-if="shareDialog"
:workflow="workflow"
@close="shareDialog = false"
/>
</div>
</template>

View File

@@ -0,0 +1,173 @@
<script setup lang="ts">
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { WorkflowShare } from '@/api/types'
import { useToast } from 'vue-toastification'
import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '@/stores'
// 国际化
const { t } = useI18n()
// 输入参数
const props = defineProps({
media: Object as PropType<WorkflowShare>,
})
// 定义事件
const emit = defineEmits(['fork', 'delete', 'close'])
// 从 provide 中获取全局设置
// 全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 提示框
const $toast = useToast()
// 处理中
const processing = ref(false)
// 删除中
const deleting = ref(false)
// 是否折叠
const isExpanded = ref(false)
// 折叠展开
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
// 复用工作流
async function doFork() {
// 开始处理
startNProgress()
try {
processing.value = true
// 请求API
const result: { [key: string]: any } = await api.post('workflow/fork', props.media)
// 工作流状态
if (result.success) {
$toast.success(t('workflow.addSuccess', { name: props.media?.share_title }))
// 完成
emit('fork', result.data.id)
} else {
$toast.error(t('workflow.addFailed', { name: props.media?.share_title, message: result.message }))
}
} catch (error) {
console.error(error)
} finally {
processing.value = false
doneNProgress()
}
}
// 删除工作流分享
async function doDelete() {
// 开始处理
startNProgress()
try {
deleting.value = true
// 请求API
const result: { [key: string]: any } = await api.delete(`workflow/share/${props.media?.id}`, {
params: {
share_uid: globalSettings.USER_UNIQUE_ID,
},
})
// 工作流状态
if (result.success) {
$toast.success(t('workflow.cancelSuccess'))
// 完成
emit('delete', result.data.id)
} else {
$toast.error(t('workflow.cancelFailed', { message: result.message }))
}
} catch (error) {
console.error(error)
} finally {
deleting.value = false
doneNProgress()
}
}
</script>
<template>
<VDialog max-width="40rem" scrollable>
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VCol>
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
<div class="flex-grow">
<VCardItem>
<VCardTitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-2 overflow-hidden text-ellipsis"
>
{{ props.media?.share_title }}
</VCardTitle>
<VCardSubtitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis"
>
{{ props.media?.share_comment }}
</VCardSubtitle>
<VList lines="one">
<VListItem class="ps-0">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">{{ t('workflow.sharer') }}</span>
<span class="text-body-1"> {{ media?.share_user }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0" v-if="media?.timer">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">{{ t('workflow.timer') }}</span>
<span class="text-body-1"> {{ media?.timer }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0" v-if="media?.actions">
<VListItemTitle class="text-center text-md-left">
<span class="font-weight-medium">{{ t('workflow.actionCount') }}</span>
<span class="text-body-1"> {{ media?.actions?.length }}</span>
</VListItemTitle>
</VListItem>
</VList>
<div class="text-center text-md-left">
<div>
<VBtn
color="primary"
:disabled="processing"
@click="doFork"
prepend-icon="mdi-heart"
:loading="processing"
class="mb-2 me-2"
>
{{ t('workflow.normalFork') }}
</VBtn>
<VBtn
v-if="
(props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID) ||
globalSettings.WORKFLOW_SHARE_MANAGE
"
color="error"
:disabled="deleting"
@click="doDelete"
prepend-icon="mdi-delete"
:loading="deleting"
class="mb-2 me-2"
>
{{ t('workflow.cancelShare') }}
</VBtn>
</div>
<div class="text-xs mt-2" v-if="props.media?.count">
<VIcon icon="mdi-fire" />{{
t('workflow.usageCount', { count: props.media?.count?.toLocaleString() })
}}
</div>
</div>
</VCardItem>
</div>
</div>
</VCol>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,122 @@
<script lang="ts" setup>
import { useToast } from 'vue-toastification'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Workflow, WorkflowShare } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
workflow: Object as PropType<Workflow>,
})
// 定义触发的自定义事件
const emit = defineEmits(['close'])
// 分享处理状态
const shareDoing = ref(false)
// 工作流分享表单
const shareForm = ref<WorkflowShare>({
workflow_id: props.workflow?.id ?? '',
share_title: props.workflow?.name ?? '',
})
// 监听props变化
watch(() => props.workflow, (newWorkflow) => {
if (newWorkflow) {
shareForm.value.workflow_id = newWorkflow.id ?? ''
shareForm.value.share_title = newWorkflow.name ?? ''
}
}, { immediate: true })
// 分享工作流
async function doShare() {
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
try {
shareDoing.value = true
const result: { [key: string]: any } = await api.post('workflow/share', shareForm.value)
shareDoing.value = false
// 提示
if (result.success) {
$toast.success(t('dialog.workflowShare.shareSuccess', { name: props.workflow?.name }))
// 通知父组件刷新
emit('close')
} else {
$toast.error(t('dialog.workflowShare.shareFailed', { name: props.workflow?.name, message: result.message }))
}
} catch (e) {
console.log(e)
}
}
// 提示框
const $toast = useToast()
</script>
<template>
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-share-outline" class="me-2" />
</template>
<VCardTitle>{{ t('dialog.workflowShare.shareWorkflow') }}</VCardTitle>
<VCardSubtitle>
{{ props.workflow?.name }}
</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}" class="pt-2">
<VRow>
<VCol cols="12">
<VTextField
v-model="shareForm.share_title"
readonly
:label="t('dialog.workflowShare.title')"
:rules="[requiredValidator]"
persistent-hint
prepend-inner-icon="mdi-format-title"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="shareForm.share_comment"
:label="t('dialog.workflowShare.description')"
:rules="[requiredValidator]"
:hint="t('dialog.workflowShare.descriptionHint')"
persistent-hint
prepend-inner-icon="mdi-comment-text-outline"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="shareForm.share_user"
:label="t('dialog.workflowShare.shareUser')"
:rules="[requiredValidator]"
:hint="t('dialog.workflowShare.shareUserHint')"
persistent-hint
prepend-inner-icon="mdi-account-outline"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn :disabled="shareDoing" @click="doShare" prepend-icon="mdi-share" class="px-5" :loading="shareDoing">
{{ t('dialog.workflowShare.confirmShare') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>