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

@@ -144,6 +144,38 @@ export interface SubscribeShare {
episode_group?: string
}
// 工作流分享
export interface WorkflowShare {
// 分享ID
id?: number
// 工作流ID
workflow_id?: string
// 分享标题
share_title?: string
// 分享说明
share_comment?: string
// 分享人
share_user?: string
// 分享人唯一ID
share_uid?: string
// 工作流名称
name?: string
// 工作流描述
description?: string
// 定时器
timer?: string
// 动作列表
actions?: any[]
// 动作流
flows?: any[]
// 上下文
context?: string
// 时间
date?: string
// 复用次数
count?: number
}
// 历史记录
export interface TransferHistory {
// ID

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>

View File

@@ -329,6 +329,10 @@ export default {
share: 'Subscription Shares',
},
},
workflowTabs: {
list: 'Workflow',
share: 'Workflow Share',
},
pluginTabs: {
installed: 'My Plugins',
market: 'Plugin Market',
@@ -488,6 +492,7 @@ export default {
dragComponentHint: 'Drag component to canvas',
task: {
edit: 'Edit Task',
share: 'Share',
continue: 'Continue',
restart: 'Restart',
run: 'Run Now',
@@ -655,6 +660,19 @@ export default {
loadPluginSettingFailed: 'Failed to load plugin settings',
},
title: 'Workflow',
share: 'Workflow Share',
searchShares: 'Search Workflow Shares',
noShareData: 'No shared workflows',
sharer: 'Sharer',
timer: 'Timer',
actionCount: 'Action Count',
normalFork: 'Fork Workflow',
cancelShare: 'Cancel Share',
cancelSuccess: 'Share cancelled successfully',
cancelFailed: 'Failed to cancel share: {message}',
usageCount: 'Used {count} times',
addSuccess: 'Forked {name} successfully!',
addFailed: 'Failed to fork {name}: {message}',
noWorkflow: 'No Workflow',
noWorkflowDescription: 'Click the add button to create a workflow task.',
},
@@ -1730,6 +1748,18 @@ export default {
shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!',
},
workflowShare: {
shareWorkflow: 'Share Workflow',
title: 'Title',
description: 'Description',
descriptionHint:
'Add a description about this workflow. Actions and flows will be included in the share by default',
shareUser: 'Share User',
shareUserHint: "Sharer's nickname",
confirmShare: 'Confirm Share',
shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!',
},
u115Auth: {
loginTitle: '115 Cloud Login',
scanQrCode: 'Please scan with WeChat or 115 client',

View File

@@ -328,6 +328,10 @@ export default {
share: '订阅分享',
},
},
workflowTabs: {
list: '工作流',
share: '工作流分享',
},
pluginTabs: {
installed: '我的插件',
market: '插件市场',
@@ -486,6 +490,7 @@ export default {
dragComponentHint: '拖动组件到画布',
task: {
edit: '编辑任务',
share: '分享',
continue: '继续执行',
restart: '重新执行',
run: '立即执行',
@@ -653,6 +658,19 @@ export default {
loadPluginSettingFailed: '加载插件设置失败',
},
title: '工作流',
share: '工作流分享',
searchShares: '搜索工作流分享',
noShareData: '暂无分享的工作流',
sharer: '分享人',
timer: '定时器',
actionCount: '动作数量',
normalFork: '复用工作流',
cancelShare: '取消分享',
cancelSuccess: '取消分享成功',
cancelFailed: '取消分享失败:{message}',
usageCount: '复用 {count} 次',
addSuccess: '复用 {name} 成功!',
addFailed: '复用 {name} 失败:{message}',
noWorkflow: '没有工作流',
noWorkflowDescription: '点击添加按钮创建工作流任务。',
},
@@ -1709,6 +1727,17 @@ export default {
shareSuccess: '{name} 分享成功!',
shareFailed: '{name} 分享失败:{message}',
},
workflowShare: {
shareWorkflow: '分享工作流',
title: '标题',
description: '说明',
descriptionHint: '填写关于该工作流的说明,工作流的动作和流程将会默认包含在分享中',
shareUser: '分享用户',
shareUserHint: '分享人的昵称',
confirmShare: '确认分享',
shareSuccess: '{name} 分享成功!',
shareFailed: '{name} 分享失败:{message}',
},
u115Auth: {
loginTitle: '115网盘登录',
scanQrCode: '请使用微信或115客户端扫码',

View File

@@ -329,6 +329,10 @@ export default {
share: '訂閱分享',
},
},
workflowTabs: {
list: '工作流',
share: '工作流分享',
},
pluginTabs: {
installed: '我的插件',
market: '插件市場',
@@ -484,6 +488,7 @@ export default {
dragComponentHint: '拖曳組件到畫布',
task: {
edit: '編輯任務',
share: '分享',
continue: '繼續',
restart: '重新開始',
run: '立即執行',
@@ -651,6 +656,19 @@ export default {
loadPluginSettingFailed: '加載插件設置失敗',
},
title: '工作流',
share: '工作流分享',
searchShares: '搜索工作流分享',
noShareData: '暫無分享的工作流',
sharer: '分享人',
timer: '定時器',
actionCount: '動作數量',
normalFork: '復用工作流',
cancelShare: '取消分享',
cancelSuccess: '取消分享成功',
cancelFailed: '取消分享失敗:{message}',
usageCount: '復用 {count} 次',
addSuccess: '復用 {name} 成功!',
addFailed: '復用 {name} 失敗:{message}',
noWorkflow: '沒有工作流',
noWorkflowDescription: '點擊添加按鈕創建工作流任務。',
},
@@ -1708,6 +1726,17 @@ export default {
shareSuccess: '{name} 分享成功!',
shareFailed: '{name} 分享失敗:{message}',
},
workflowShare: {
shareWorkflow: '分享工作流',
title: '標題',
description: '說明',
descriptionHint: '填寫關於該工作流的說明,工作流的動作和流程將會默認包含在分享中',
shareUser: '分享用戶',
shareUserHint: '分享人的暱稱',
confirmShare: '確認分享',
shareSuccess: '{name} 分享成功!',
shareFailed: '{name} 分享失敗:{message}',
},
u115Auth: {
loginTitle: '115網盤登錄',
scanQrCode: '請使用微信或115客戶端掃碼',

View File

@@ -1,9 +1,133 @@
<script setup lang="ts">
import WorkflowListView from '@/views/workflow/WorkflowListView.vue'
import WorkflowShareView from '@/views/workflow/WorkflowShareView.vue'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import { useI18n } from 'vue-i18n'
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
import { getWorkflowTabs } from '@/router/i18n-menu'
// 国际化
const { t } = useI18n()
const route = useRoute()
const activeTab = ref((route.query.tab as string) || '')
const shareViewKey = ref(0)
// 获取标签页
const workflowTabs = computed(() => {
return getWorkflowTabs()
})
// 新增工作流对话框
const addWorkflowDialog = ref(false)
// 工作流列表刷新key
const workflowListKey = ref(0)
// 分享搜索词
const shareKeyword = ref('')
// 搜索分享
const searchShares = () => {
shareViewKey.value++
}
// VMenu activator选择器
const searchActivator = computed(() => '[data-menu-activator="search-btn"]')
// 使用动态标签页
const { registerHeaderTab } = useDynamicHeaderTab()
// 注册动态标签页
registerHeaderTab({
items: workflowTabs.value,
modelValue: activeTab,
appendButtons: [
{
icon: 'mdi-movie-search-outline',
variant: 'text',
color: computed(() => (shareKeyword.value ? 'primary' : 'gray')),
class: 'settings-icon-button',
dataAttr: 'search-btn',
action: () => {
// 这里可以添加搜索弹窗逻辑
console.log('Search workflow shares')
},
show: computed(() => activeTab.value === 'share'),
},
{
icon: 'mdi-plus',
variant: 'text',
color: 'gray',
class: 'settings-icon-button',
action: () => {
addWorkflowDialog.value = true
},
show: computed(() => activeTab.value === 'list'),
},
],
})
// 注册动态标签页
onMounted(() => {
// 设置初始activeTab值
if (!activeTab.value && workflowTabs.value.length > 0) {
activeTab.value = workflowTabs.value[0].tab
}
})
</script>
<template>
<div>
<WorkflowListView />
<VWindow v-model="activeTab" class="disable-tab-transition content-window" :touch="false">
<VWindowItem value="list">
<transition name="fade-slide" appear>
<div>
<WorkflowListView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="share">
<transition name="fade-slide" appear>
<div>
<div class="mb-4">
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="shareKeyword"
:label="t('workflow.searchShares')"
prepend-inner-icon="mdi-magnify"
clearable
@keyup.enter="searchShares"
@click:clear="searchShares"
/>
</VCol>
<VCol cols="12" md="6" class="d-flex align-center">
<VBtn @click="searchShares" prepend-icon="mdi-magnify" class="me-2">
{{ t('workflow.searchShares') }}
</VBtn>
</VCol>
</VRow>
</div>
<WorkflowShareView :keyword="shareKeyword" :key="shareViewKey" />
</div>
</transition>
</VWindowItem>
</VWindow>
<!-- 新增工作流对话框 -->
<WorkflowAddEditDialog
v-if="addWorkflowDialog"
v-model="addWorkflowDialog"
@close="addWorkflowDialog = false"
@save="addWorkflowDialog = false"
/>
</div>
</template>
<style scoped>
.content-window {
margin-block-start: 0;
}
</style>

View File

@@ -293,3 +293,21 @@ export function getDiscoverTabs() {
},
]
}
// 获取工作流标签页
export function getWorkflowTabs() {
const { t } = useI18n()
return [
{
title: t('workflowTabs.list'),
tab: 'list',
icon: 'mdi-format-list-bulleted',
},
{
title: t('workflowTabs.share'),
tab: 'share',
icon: 'mdi-share',
},
]
}

View File

@@ -236,6 +236,10 @@ body {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.grid-workflow-share-card {
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}

View File

@@ -4,7 +4,6 @@ import { Workflow } from '@/api/types'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import { usePWA } from '@/composables/usePWA'
@@ -49,14 +48,6 @@ onMounted(() => {
onActivated(() => {
fetchData()
})
// 使用动态按钮钩子 新增
useDynamicButton({
icon: 'mdi-plus',
onClick: () => {
addDialog.value = true
},
})
</script>
<template>
<div>
@@ -72,21 +63,4 @@ useDynamicButton({
:error-description="t('workflow.noWorkflowDescription')"
/>
</div>
<!-- 新增按钮 -->
<Teleport to="body" v-if="route.path === '/workflow'">
<VFab
v-if="isRefreshed && !appMode"
icon="mdi-plus"
location="bottom"
size="x-large"
fixed
app
appear
:class="{ 'mb-12': appMode }"
@click="addDialog = true"
/>
</Teleport>
<!-- 新增对话框 -->
<WorkflowAddEditDialog v-if="addDialog" v-model="addDialog" @close="addDialog = false" @save="addDone" />
</template>

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
import api from '@/api'
import type { WorkflowShare } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import WorkflowShareCard from '@/components/cards/WorkflowShareCard.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 定义输入参数
const props = defineProps({
// 过滤关键字
keyword: String,
})
// 判断是否有滚动条
function hasScroll() {
return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
}
// API
const apipath = 'workflow/shares'
// 当前页码
const page = ref(1)
// 搜索关键字
const keyword = ref(props.keyword)
// 是否加载中
const loading = ref(false)
// 是否加载完成
const isRefreshed = ref(false)
// 数据列表
const dataList = ref<WorkflowShare[]>([])
const currData = ref<WorkflowShare[]>([])
// 拼装参数
function getParams() {
let params = {
page: page.value,
count: 30,
name: keyword.value,
}
return params
}
// 获取列表数据
async function fetchData({ done }: { done: any }) {
try {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 加载到满屏或者加载出错
if (!hasScroll()) {
// 加载多次
while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(apipath, {
params: getParams(),
})
// 取消加载中
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
}
} else {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(apipath, {
params: getParams(),
})
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
}
}
} catch (error) {
console.error(error)
// 返回加载失败
done('error')
}
}
// 将数据从列表中移除
function removeData(id: number) {
dataList.value = dataList.value.filter(item => item.id !== id)
}
</script>
<template>
<VPageContentTitle v-if="keyword" :title="`${t('common.search')}${keyword}`" />
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData">
<template #loading />
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-workflow-share-card" tabindex="0">
<div v-for="data in dataList" :key="data.id">
<WorkflowShareCard :media="data" @delete="removeData(data.id || 0)" />
</div>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
:error-title="t('common.noData')"
:error-description="keyword ? t('common.noContent') : t('workflow.noShareData')"
/>
</VInfiniteScroll>
</template>

View File

@@ -1068,6 +1068,11 @@
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz"
integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
version "1.0.4"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz"
integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
"@img/sharp-linux-x64@0.33.5":
version "0.33.5"
resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz"
@@ -1075,6 +1080,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.0.4"
"@img/sharp-linuxmusl-x64@0.33.5":
version "0.33.5"
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz"
integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.0.4"
"@intlify/bundle-utils@^10.0.1":
version "10.0.1"
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-10.0.1.tgz"
@@ -1292,6 +1304,11 @@
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
"@parcel/watcher-linux-x64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher@^2.4.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"
@@ -1385,6 +1402,11 @@
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz"
integrity sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==
"@rollup/rollup-linux-x64-musl@4.40.1":
version "4.40.1"
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz"
integrity sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz"
@@ -1437,6 +1459,11 @@
resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz"
integrity sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==
"@swc/core-linux-x64-musl@1.12.9":
version "1.12.9"
resolved "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz"
integrity sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==
"@swc/core@^1.10.16":
version "1.12.9"
resolved "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz"
@@ -1692,6 +1719,11 @@
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz"
integrity sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==
"@unrs/resolver-binding-linux-x64-musl@1.7.2":
version "1.7.2"
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz"
integrity sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==
"@vitejs/plugin-vue-jsx@^4.1.1":
version "4.1.2"
resolved "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.1.2.tgz"