mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-10 02:00:44 +08:00
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:
121
src/components/cards/WorkflowShareCard.vue
Normal file
121
src/components/cards/WorkflowShareCard.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
173
src/components/dialog/ForkWorkflowDialog.vue
Normal file
173
src/components/dialog/ForkWorkflowDialog.vue
Normal 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>
|
||||
122
src/components/dialog/WorkflowShareDialog.vue
Normal file
122
src/components/dialog/WorkflowShareDialog.vue
Normal 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>
|
||||
Reference in New Issue
Block a user