mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-28 19:59:52 +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:
@@ -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
|
||||
|
||||
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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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客户端扫码',
|
||||
|
||||
@@ -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客戶端掃碼',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
139
src/views/workflow/WorkflowShareView.vue
Normal file
139
src/views/workflow/WorkflowShareView.vue
Normal 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>
|
||||
32
yarn.lock
32
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user