mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-25 17:44:13 +08:00
refactor: enhance dynamic button system to support menus, reactive properties, and improved PWA floating action button integration
This commit is contained in:
@@ -5,6 +5,8 @@ import FileNavigator from './filebrowser/FileNavigator.vue'
|
||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||
import { storageIconDict } from '@/api/constants'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
|
||||
// LocalStorage keys
|
||||
const SORT_KEY = 'fileBrowser.sort'
|
||||
@@ -33,6 +35,9 @@ const props = defineProps({
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged'])
|
||||
const route = useRoute()
|
||||
const { appMode } = usePWA()
|
||||
const toolbarRef = ref<InstanceType<typeof FileToolbar> | null>(null)
|
||||
|
||||
const fileIcons = {
|
||||
// 压缩包
|
||||
@@ -123,6 +128,18 @@ const fileIcons = {
|
||||
other: 'mdi-file-outline',
|
||||
}
|
||||
|
||||
function openNewFolderDialog() {
|
||||
toolbarRef.value?.openNewFolderDialog()
|
||||
}
|
||||
|
||||
const showFloatingNewFolderAction = computed(() => route.path === '/filemanager')
|
||||
|
||||
useDynamicButton({
|
||||
icon: 'mdi-folder-plus-outline',
|
||||
onClick: openNewFolderDialog,
|
||||
show: computed(() => appMode.value && showFloatingNewFolderAction.value),
|
||||
})
|
||||
|
||||
// 加载次数
|
||||
const loading = ref(0)
|
||||
|
||||
@@ -254,12 +271,14 @@ function stopDrag() {
|
||||
<div class="mx-auto" :loading="loading > 0">
|
||||
<div v-if="item">
|
||||
<FileToolbar
|
||||
ref="toolbarRef"
|
||||
:sort="sort"
|
||||
:item="item"
|
||||
:itemstack="itemstack"
|
||||
:storages="storagesArray"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:show-new-folder-button="!showFloatingNewFolderAction"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@@ -301,6 +320,18 @@ function stopDrag() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body" v-if="!appMode && showFloatingNewFolderAction">
|
||||
<VFab
|
||||
icon="mdi-folder-plus-outline"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="openNewFolderDialog"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -30,6 +30,10 @@ const inProps = defineProps({
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
showNewFolderButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
@@ -109,11 +113,20 @@ async function mkdir() {
|
||||
emit('foldercreated')
|
||||
}
|
||||
|
||||
function openNewFolderDialog() {
|
||||
newFolderName.value = ''
|
||||
newFolderPopper.value = true
|
||||
}
|
||||
|
||||
// 计算排序图标
|
||||
const sortIcon = computed(() => {
|
||||
if (inProps.sort === 'time') return 'mdi-sort-clock-ascending-outline'
|
||||
else return 'mdi-sort-alphabetical-ascending'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
openNewFolderDialog,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -165,9 +178,9 @@ const sortIcon = computed(() => {
|
||||
</IconBtn>
|
||||
<!-- 新建文件夹 -->
|
||||
<VDialog v-model="newFolderPopper" max-width="35rem">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn>
|
||||
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
|
||||
<template v-if="showNewFolderButton" #activator="{ props }">
|
||||
<IconBtn v-bind="props">
|
||||
<VIcon icon="mdi-folder-plus-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard>
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import { ref, inject, nextTick, onMounted, onActivated, onDeactivated, onUnmounted } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
unref,
|
||||
watch,
|
||||
type ComputedRef,
|
||||
type Ref,
|
||||
} from 'vue'
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
interface Window {
|
||||
__VUE_INJECT_DYNAMIC_BUTTON__?: (button: any) => void
|
||||
__VUE_UNINJECT_DYNAMIC_BUTTON__?: () => void
|
||||
}
|
||||
}
|
||||
|
||||
type MaybeRefValue<T> = T | Ref<T> | ComputedRef<T>
|
||||
|
||||
interface DynamicButtonMenuItem {
|
||||
title: string
|
||||
icon?: string
|
||||
color?: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
function resolveMaybeRef<T>(value: MaybeRefValue<T> | undefined): T | undefined
|
||||
function resolveMaybeRef<T>(value: MaybeRefValue<T> | undefined, fallback: T): T
|
||||
function resolveMaybeRef<T>(value: MaybeRefValue<T> | undefined, fallback?: T) {
|
||||
return value !== undefined ? unref(value) : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态按钮钩子函数
|
||||
*
|
||||
@@ -23,12 +52,14 @@ declare global {
|
||||
* })
|
||||
*/
|
||||
export function useDynamicButton(options: {
|
||||
icon: string
|
||||
onClick: () => void
|
||||
icon: MaybeRefValue<string>
|
||||
onClick?: () => void
|
||||
menuItems?: MaybeRefValue<DynamicButtonMenuItem[] | undefined>
|
||||
show?: MaybeRefValue<boolean>
|
||||
autoRegister?: boolean // 是否自动注册,默认为true
|
||||
}) {
|
||||
// 提取配置
|
||||
const { icon, onClick, autoRegister = true } = options
|
||||
const { icon, onClick, menuItems, show, autoRegister = true } = options
|
||||
|
||||
// 动态按钮相关
|
||||
const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null)
|
||||
@@ -36,22 +67,42 @@ export function useDynamicButton(options: {
|
||||
|
||||
// 按钮注册状态
|
||||
const dynamicButtonRegistered = ref(false)
|
||||
const componentActive = ref(false)
|
||||
|
||||
const resolvedIcon = computed(() => resolveMaybeRef(icon, 'mdi-plus'))
|
||||
const resolvedShow = computed(() => resolveMaybeRef(show, true))
|
||||
const resolvedMenuItems = computed(() => resolveMaybeRef(menuItems))
|
||||
|
||||
function buildDynamicButton() {
|
||||
const buttonMenuItems = resolvedMenuItems.value
|
||||
|
||||
return {
|
||||
icon: resolvedIcon.value,
|
||||
action: onClick || (() => {}),
|
||||
show: resolvedShow.value,
|
||||
menuItems: buttonMenuItems && buttonMenuItems.length > 0 ? buttonMenuItems : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// 注册动态按钮
|
||||
function setupDynamicButton() {
|
||||
// 避免重复注册
|
||||
if (dynamicButtonRegistered.value) return
|
||||
if (!componentActive.value) return
|
||||
|
||||
const button = buildDynamicButton()
|
||||
|
||||
if (!button.show) {
|
||||
cleanupDynamicButton()
|
||||
return
|
||||
}
|
||||
|
||||
// 确保注册方法存在
|
||||
if (!registerDynamicButton) {
|
||||
// 尝试获取全局注册方法
|
||||
const tryUseGlobalMethod = () => {
|
||||
if (!componentActive.value) return false
|
||||
|
||||
if (typeof window !== 'undefined' && window.__VUE_INJECT_DYNAMIC_BUTTON__) {
|
||||
window.__VUE_INJECT_DYNAMIC_BUTTON__({
|
||||
icon,
|
||||
action: onClick,
|
||||
show: true,
|
||||
})
|
||||
window.__VUE_INJECT_DYNAMIC_BUTTON__(button)
|
||||
dynamicButtonRegistered.value = true
|
||||
return true
|
||||
}
|
||||
@@ -68,11 +119,9 @@ export function useDynamicButton(options: {
|
||||
|
||||
// 如果注册方法存在,直接注册
|
||||
nextTick(() => {
|
||||
registerDynamicButton({
|
||||
icon,
|
||||
action: onClick,
|
||||
show: true,
|
||||
})
|
||||
if (!componentActive.value) return
|
||||
|
||||
registerDynamicButton(button)
|
||||
dynamicButtonRegistered.value = true
|
||||
})
|
||||
}
|
||||
@@ -82,17 +131,24 @@ export function useDynamicButton(options: {
|
||||
if (unregisterDynamicButton && dynamicButtonRegistered.value) {
|
||||
unregisterDynamicButton()
|
||||
dynamicButtonRegistered.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.__VUE_UNINJECT_DYNAMIC_BUTTON__) {
|
||||
window.__VUE_UNINJECT_DYNAMIC_BUTTON__()
|
||||
dynamicButtonRegistered.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法:手动打开对话框
|
||||
function openDialog() {
|
||||
onClick()
|
||||
onClick?.()
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
if (autoRegister) {
|
||||
onMounted(() => {
|
||||
componentActive.value = true
|
||||
// 延迟执行,确保Footer组件已加载
|
||||
setTimeout(() => {
|
||||
setupDynamicButton()
|
||||
@@ -100,18 +156,27 @@ export function useDynamicButton(options: {
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
componentActive.value = true
|
||||
// 重置注册状态,确保每次激活时都重新注册
|
||||
dynamicButtonRegistered.value = false
|
||||
setupDynamicButton()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
componentActive.value = false
|
||||
cleanupDynamicButton()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
componentActive.value = false
|
||||
cleanupDynamicButton()
|
||||
})
|
||||
|
||||
watch([resolvedIcon, resolvedShow, resolvedMenuItems], () => {
|
||||
if (!componentActive.value) return
|
||||
|
||||
setupDynamicButton()
|
||||
}, { deep: true })
|
||||
}
|
||||
|
||||
// 返回控制函数和状态
|
||||
|
||||
@@ -147,6 +147,7 @@ const unregisterDynamicButton = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 确保在浏览器环境中
|
||||
;(window as any).__VUE_INJECT_DYNAMIC_BUTTON__ = registerDynamicButton
|
||||
;(window as any).__VUE_UNINJECT_DYNAMIC_BUTTON__ = unregisterDynamicButton
|
||||
}
|
||||
|
||||
// 提供给其他组件使用
|
||||
@@ -160,6 +161,7 @@ onUnmounted(() => {
|
||||
// 清理全局方法
|
||||
if (typeof window !== 'undefined') {
|
||||
delete (window as any).__VUE_INJECT_DYNAMIC_BUTTON__
|
||||
delete (window as any).__VUE_UNINJECT_DYNAMIC_BUTTON__
|
||||
}
|
||||
})
|
||||
|
||||
@@ -241,7 +243,11 @@ const hasDynamicButtonMenu = computed(() => Boolean(dynamicButton.value?.menuIte
|
||||
rounded="pill"
|
||||
class="footer-nav-btn"
|
||||
>
|
||||
<VIcon color="secondary" :icon="dynamicButton?.icon || 'mdi-plus'" size="28"></VIcon>
|
||||
<VIcon
|
||||
color="secondary"
|
||||
:icon="hasDynamicButtonMenu ? 'mdi-chevron-up' : dynamicButton?.icon || 'mdi-plus'"
|
||||
size="28"
|
||||
></VIcon>
|
||||
</VBtn>
|
||||
<VMenu v-if="hasDynamicButtonMenu" activator="parent" location="top end" close-on-content-click>
|
||||
<VList>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash-es'
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
|
||||
@@ -6,18 +7,23 @@ import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
import SubscribeShareStatisticsDialog from '@/components/dialog/SubscribeShareStatisticsDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
import { getSubscribeMovieTabs, getSubscribeTvTabs } from '@/router/i18n-menu'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const { appMode } = usePWA()
|
||||
|
||||
const subType = route.meta.subType?.toString()
|
||||
const subId = ref(route.query.id as string)
|
||||
const activeTab = ref((route.query.tab as string) || '')
|
||||
const shareViewKey = ref(0)
|
||||
const subscribeListViewRef = ref<InstanceType<typeof SubscribeListView> | null>(null)
|
||||
|
||||
// 获取标签页
|
||||
const subscribeTabs = computed(() => {
|
||||
@@ -48,12 +54,7 @@ const subscribeStatusFilter = ref<string | null>(null)
|
||||
|
||||
// 分享搜索词
|
||||
const shareKeyword = ref('')
|
||||
|
||||
// 搜索分享
|
||||
const searchShares = () => {
|
||||
searchShareDialog.value = false
|
||||
shareViewKey.value++
|
||||
}
|
||||
const shareKeywordInput = ref('')
|
||||
|
||||
// 筛选选项
|
||||
const filterOptions = computed(() => {
|
||||
@@ -103,7 +104,98 @@ function selectFilter(value: string) {
|
||||
|
||||
// VMenu activator选择器
|
||||
const filterActivator = computed(() => '[data-menu-activator="filter-btn"]')
|
||||
const searchActivator = computed(() => '[data-menu-activator="search-btn"]')
|
||||
const searchActivator = computed(() => '[data-menu-activator="share-filter-btn"]')
|
||||
|
||||
const showDefaultRuleAction = computed(() => activeTab.value === 'mysub')
|
||||
const showSubscribeHistoryAction = computed(() => showDefaultRuleAction.value && userStore.superUser)
|
||||
const showShareStatisticsAction = computed(() => activeTab.value === 'share')
|
||||
|
||||
function openDefaultRuleDialog() {
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
|
||||
function openSubscribeHistoryDialog() {
|
||||
subscribeListViewRef.value?.openHistoryDialog()
|
||||
}
|
||||
|
||||
function openShareStatisticsDialog() {
|
||||
shareStatisticsDialog.value = true
|
||||
}
|
||||
|
||||
const shareKeywordUpdater = debounce((keyword: string) => {
|
||||
shareKeyword.value = keyword.trim()
|
||||
}, 300)
|
||||
|
||||
watch(shareKeywordInput, newKeyword => {
|
||||
shareKeywordUpdater(newKeyword || '')
|
||||
})
|
||||
|
||||
watch(activeTab, newTab => {
|
||||
if (newTab !== 'share') {
|
||||
searchShareDialog.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
shareKeywordUpdater.cancel()
|
||||
})
|
||||
|
||||
const subscribeDynamicMenuItems = computed(() => {
|
||||
locale.value
|
||||
|
||||
if (!appMode.value) return undefined
|
||||
|
||||
if (activeTab.value === 'mysub') {
|
||||
const items: Array<{ title: string; icon: string; action: () => void }> = []
|
||||
|
||||
if (showSubscribeHistoryAction.value) {
|
||||
items.push({
|
||||
title: t('components.subscribeHistory.title', { type: subType }),
|
||||
icon: 'mdi-history',
|
||||
action: openSubscribeHistoryDialog,
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: t('components.subscribeEdit.titleDefault'),
|
||||
icon: 'mdi-clipboard-edit-outline',
|
||||
action: openDefaultRuleDialog,
|
||||
})
|
||||
|
||||
return items.length > 1 ? items : undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const subscribeDynamicIcon = computed(() => {
|
||||
if (showShareStatisticsAction.value) return 'mdi-chart-line'
|
||||
if (showSubscribeHistoryAction.value) return 'mdi-history'
|
||||
return 'mdi-clipboard-edit-outline'
|
||||
})
|
||||
|
||||
function handleSubscribeDynamicAction() {
|
||||
if (showShareStatisticsAction.value) {
|
||||
openShareStatisticsDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (showSubscribeHistoryAction.value) {
|
||||
openSubscribeHistoryDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (showDefaultRuleAction.value) {
|
||||
openDefaultRuleDialog()
|
||||
}
|
||||
}
|
||||
|
||||
useDynamicButton({
|
||||
icon: subscribeDynamicIcon,
|
||||
onClick: handleSubscribeDynamicAction,
|
||||
menuItems: subscribeDynamicMenuItems,
|
||||
show: computed(() => appMode.value && (showDefaultRuleAction.value || showShareStatisticsAction.value)),
|
||||
})
|
||||
|
||||
// 使用动态标签页
|
||||
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||
@@ -137,37 +229,16 @@ registerHeaderTab({
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-chart-line',
|
||||
icon: 'mdi-filter-multiple-outline',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
color: computed(() => (shareKeywordInput.value ? 'primary' : 'gray')),
|
||||
class: 'settings-icon-button',
|
||||
dataAttr: 'statistics-btn',
|
||||
action: () => {
|
||||
shareStatisticsDialog.value = true
|
||||
},
|
||||
show: computed(() => activeTab.value === 'share'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-movie-search-outline',
|
||||
variant: 'text',
|
||||
color: computed(() => (shareKeyword.value ? 'primary' : 'gray')),
|
||||
class: 'settings-icon-button',
|
||||
dataAttr: 'search-btn',
|
||||
dataAttr: 'share-filter-btn',
|
||||
action: () => {
|
||||
searchShareDialog.value = true
|
||||
},
|
||||
show: computed(() => activeTab.value === 'share'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clipboard-edit-outline',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
class: 'settings-icon-button',
|
||||
action: () => {
|
||||
subscribeEditDialog.value = true
|
||||
},
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -187,6 +258,7 @@ onMounted(() => {
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribeListView
|
||||
ref="subscribeListViewRef"
|
||||
:type="subType"
|
||||
:subid="subId"
|
||||
:keyword="subscribeFilter"
|
||||
@@ -205,7 +277,7 @@ onMounted(() => {
|
||||
<VWindowItem value="share">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribeShareView :keyword="shareKeyword" :key="shareViewKey" />
|
||||
<SubscribeShareView :keyword="shareKeyword" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
@@ -260,30 +332,65 @@ onMounted(() => {
|
||||
<Teleport to="body" v-if="searchShareDialog">
|
||||
<VMenu
|
||||
v-model="searchShareDialog"
|
||||
width="25rem"
|
||||
:close-on-content-click="false"
|
||||
:activator="searchActivator"
|
||||
location="bottom end"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-movie-search-outline" class="mr-2" />
|
||||
{{ t('subscribe.searchShares') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="searchShareDialog = false" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextField v-model="shareKeyword" :label="t('subscribe.keyword')" clearable density="comfortable">
|
||||
<template #append>
|
||||
<VBtn prepend-icon="mdi-magnify" color="primary" @click="searchShares">{{ t('common.search') }}</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
<VCard min-width="260" max-width="320">
|
||||
<div class="px-3 pt-3 pb-1">
|
||||
<VTextField
|
||||
v-model="shareKeywordInput"
|
||||
:placeholder="t('subscribe.keyword')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</Teleport>
|
||||
|
||||
<Teleport to="body" v-if="!appMode && route.path.startsWith(`/subscribe/${subType === '电影' ? 'movie' : 'tv'}`)">
|
||||
<div>
|
||||
<VFab
|
||||
v-if="showSubscribeHistoryAction"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="openSubscribeHistoryDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showDefaultRuleAction"
|
||||
icon="mdi-clipboard-edit-outline"
|
||||
color="primary"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
:class="{ 'mb-16': showSubscribeHistoryAction }"
|
||||
@click="openDefaultRuleDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showShareStatisticsAction"
|
||||
icon="mdi-chart-line"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="openShareStatisticsDialog"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
|
||||
@@ -1,42 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash-es'
|
||||
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 { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { getWorkflowTabs } from '@/router/i18n-menu'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
const { appMode } = usePWA()
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'list')
|
||||
const shareViewKey = ref(0)
|
||||
const listViewKey = ref(0)
|
||||
const workflowListViewRef = ref<InstanceType<typeof WorkflowListView> | null>(null)
|
||||
|
||||
// 获取标签页
|
||||
const workflowTabs = computed(() => {
|
||||
return getWorkflowTabs(t)
|
||||
})
|
||||
|
||||
// 新增工作流对话框
|
||||
const addWorkflowDialog = ref(false)
|
||||
|
||||
// 分享搜索词
|
||||
const shareKeyword = ref('')
|
||||
const shareKeywordInput = ref('')
|
||||
|
||||
// 搜索分享对话框
|
||||
const searchShareDialog = ref(false)
|
||||
|
||||
// 搜索分享激活器
|
||||
const searchActivator = computed(() => '[data-menu-activator="search-btn"]')
|
||||
const searchActivator = computed(() => '[data-menu-activator="share-filter-btn"]')
|
||||
|
||||
// 搜索分享
|
||||
const searchShares = () => {
|
||||
shareViewKey.value++
|
||||
function openAddWorkflowDialog() {
|
||||
workflowListViewRef.value?.openAddDialog()
|
||||
}
|
||||
|
||||
const shareKeywordUpdater = debounce((keyword: string) => {
|
||||
shareKeyword.value = keyword.trim()
|
||||
}, 300)
|
||||
|
||||
watch(shareKeywordInput, newKeyword => {
|
||||
shareKeywordUpdater(newKeyword || '')
|
||||
})
|
||||
|
||||
watch(activeTab, newTab => {
|
||||
if (newTab !== 'share') {
|
||||
searchShareDialog.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
shareKeywordUpdater.cancel()
|
||||
})
|
||||
|
||||
useDynamicButton({
|
||||
icon: 'mdi-plus',
|
||||
onClick: openAddWorkflowDialog,
|
||||
show: computed(() => appMode.value && activeTab.value === 'list'),
|
||||
})
|
||||
|
||||
// 使用动态标签页
|
||||
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||
|
||||
@@ -46,11 +70,11 @@ registerHeaderTab({
|
||||
modelValue: activeTab,
|
||||
appendButtons: [
|
||||
{
|
||||
icon: 'mdi-search',
|
||||
icon: 'mdi-filter-multiple-outline',
|
||||
variant: 'text',
|
||||
color: computed(() => (shareKeyword.value ? 'primary' : 'gray')),
|
||||
color: computed(() => (shareKeywordInput.value ? 'primary' : 'gray')),
|
||||
class: 'settings-icon-button',
|
||||
dataAttr: 'search-btn',
|
||||
dataAttr: 'share-filter-btn',
|
||||
show: computed(() => activeTab.value === 'share'),
|
||||
action: () => {
|
||||
searchShareDialog.value = true
|
||||
@@ -74,54 +98,54 @@ onMounted(() => {
|
||||
<VWindowItem value="list">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<WorkflowListView :key="listViewKey" />
|
||||
<WorkflowListView ref="workflowListViewRef" :key="listViewKey" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="share">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<WorkflowShareView :keyword="shareKeyword" :key="shareViewKey" @update="listViewKey++" />
|
||||
<WorkflowShareView :keyword="shareKeyword" @update="listViewKey++" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
|
||||
<!-- 新增工作流对话框 -->
|
||||
<WorkflowAddEditDialog
|
||||
v-if="addWorkflowDialog"
|
||||
v-model="addWorkflowDialog"
|
||||
@close="addWorkflowDialog = false"
|
||||
@save="addWorkflowDialog = false"
|
||||
/>
|
||||
|
||||
<!-- 搜索工作流分享弹窗 -->
|
||||
<Teleport to="body" v-if="searchShareDialog">
|
||||
<VMenu
|
||||
v-model="searchShareDialog"
|
||||
width="25rem"
|
||||
:close-on-content-click="false"
|
||||
:activator="searchActivator"
|
||||
location="bottom end"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-movie-search-outline" class="mr-2" />
|
||||
{{ t('workflow.searchShares') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="searchShareDialog = false" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextField v-model="shareKeyword" :label="t('workflow.searchShares')" clearable density="comfortable">
|
||||
<template #append>
|
||||
<VBtn prepend-icon="mdi-magnify" color="primary" @click="searchShares">{{ t('common.search') }}</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
<VCard min-width="260" max-width="320">
|
||||
<div class="px-3 pt-3 pb-1">
|
||||
<VTextField
|
||||
v-model="shareKeywordInput"
|
||||
:placeholder="t('workflow.searchShares')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</Teleport>
|
||||
|
||||
<Teleport to="body" v-if="!appMode && route.path === '/workflow' && activeTab === 'list'">
|
||||
<VFab
|
||||
icon="mdi-plus"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="openAddWorkflowDialog"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -79,26 +79,6 @@ registerHeaderTab({
|
||||
},
|
||||
show: computed(() => activeTab.value === 'market'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-store-cog',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
class: 'settings-icon-button',
|
||||
action: () => {
|
||||
MarketSettingDialog.value = true
|
||||
},
|
||||
show: computed(() => activeTab.value === 'market'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-folder-plus',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
class: 'settings-icon-button',
|
||||
action: () => {
|
||||
showNewFolderDialog()
|
||||
},
|
||||
show: computed(() => activeTab.value === 'installed' && !currentFolder.value),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-arrow-left',
|
||||
variant: 'text',
|
||||
@@ -915,12 +895,56 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 使用动态按钮钩子
|
||||
function openPluginSearchDialog() {
|
||||
SearchDialog.value = true
|
||||
}
|
||||
|
||||
function openMarketSettingDialog() {
|
||||
MarketSettingDialog.value = true
|
||||
}
|
||||
|
||||
const showSearchAction = computed(() => activeTab.value === 'installed' || activeTab.value === 'market')
|
||||
const showNewFolderAction = computed(() => activeTab.value === 'installed' && !currentFolder.value)
|
||||
const showMarketSettingAction = computed(() => activeTab.value === 'market')
|
||||
|
||||
const pluginDynamicMenuItems = computed(() => {
|
||||
locale.value
|
||||
|
||||
if (!appMode.value) return undefined
|
||||
if (!showSearchAction.value) return undefined
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t('plugin.searchPlugins'),
|
||||
icon: 'mdi-magnify',
|
||||
action: openPluginSearchDialog,
|
||||
},
|
||||
]
|
||||
|
||||
if (showNewFolderAction.value) {
|
||||
items.push({
|
||||
title: t('plugin.newFolder'),
|
||||
icon: 'mdi-folder-plus',
|
||||
action: showNewFolderDialog,
|
||||
})
|
||||
}
|
||||
|
||||
if (showMarketSettingAction.value) {
|
||||
items.push({
|
||||
title: t('components.pluginMarketSetting.title'),
|
||||
icon: 'mdi-store-cog',
|
||||
action: openMarketSettingDialog,
|
||||
})
|
||||
}
|
||||
|
||||
return items.length > 1 ? items : undefined
|
||||
})
|
||||
|
||||
useDynamicButton({
|
||||
icon: 'mdi-magnify',
|
||||
onClick: () => {
|
||||
SearchDialog.value = true
|
||||
},
|
||||
onClick: openPluginSearchDialog,
|
||||
menuItems: pluginDynamicMenuItems,
|
||||
show: computed(() => appMode.value && showSearchAction.value && isRefreshed.value),
|
||||
})
|
||||
|
||||
// 获取插件文件夹配置
|
||||
@@ -1585,9 +1609,8 @@ function onDragStartPlugin(evt: any) {
|
||||
|
||||
<!-- 插件搜索图标 -->
|
||||
<Teleport to="body" v-if="route.path === '/plugins'">
|
||||
<div v-if="isRefreshed">
|
||||
<div v-if="isRefreshed && !appMode && showSearchAction">
|
||||
<VFab
|
||||
v-if="!appMode"
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom"
|
||||
@@ -1595,8 +1618,31 @@ function onDragStartPlugin(evt: any) {
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="SearchDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
@click="openPluginSearchDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showNewFolderAction"
|
||||
icon="mdi-folder-plus"
|
||||
color="primary"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
class="mb-16"
|
||||
@click="showNewFolderDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showMarketSettingAction"
|
||||
icon="mdi-store-cog"
|
||||
color="warning"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
class="mb-16"
|
||||
@click="openMarketSettingDialog"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -12,70 +12,13 @@ import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// i18n
|
||||
const { t } = useI18n()
|
||||
|
||||
// 动态按钮相关
|
||||
interface DynamicButton {
|
||||
icon: string
|
||||
action: () => void
|
||||
show: boolean
|
||||
routePath?: string
|
||||
menuItems?: {
|
||||
title: string
|
||||
icon?: string
|
||||
color?: string
|
||||
action: () => void
|
||||
}[]
|
||||
}
|
||||
|
||||
const injectedRegisterDynamicButton = inject<((button: DynamicButton) => void) | null>('registerDynamicButton', null)
|
||||
const injectedUnregisterDynamicButton = inject<(() => void) | null>('unregisterDynamicButton', null)
|
||||
|
||||
function registerHistoryDynamicButton(show: boolean) {
|
||||
const button: DynamicButton = {
|
||||
icon: 'mdi-chevron-up',
|
||||
show,
|
||||
action: () => {},
|
||||
menuItems: [
|
||||
{
|
||||
title: t('transferHistory.actions.batchRedo'),
|
||||
icon: 'mdi-redo-variant',
|
||||
action: () => {
|
||||
retransferBatch()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchDelete'),
|
||||
icon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
action: () => {
|
||||
removeHistoryBatch()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (injectedRegisterDynamicButton) {
|
||||
injectedRegisterDynamicButton(button)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && (window as any).__VUE_INJECT_DYNAMIC_BUTTON__) {
|
||||
;(window as any).__VUE_INJECT_DYNAMIC_BUTTON__(button)
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterHistoryDynamicButton() {
|
||||
if (injectedUnregisterDynamicButton) {
|
||||
injectedUnregisterDynamicButton()
|
||||
return
|
||||
}
|
||||
}
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// 全局设置
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
@@ -698,32 +641,55 @@ const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) =>
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中项变化,更新动态按钮
|
||||
watch(
|
||||
() => selected.value.length,
|
||||
newLength => {
|
||||
if (appMode) {
|
||||
registerHistoryDynamicButton(newLength > 0)
|
||||
}
|
||||
const historyDynamicIcon = computed(() => (selected.value.length > 0 ? 'mdi-chevron-up' : 'mdi-timer-sand-paused'))
|
||||
const historyDynamicMenuItems = computed(() => {
|
||||
locale.value
|
||||
|
||||
if (selected.value.length === 0) return undefined
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('components.transferQueue.title'),
|
||||
icon: 'mdi-timer-sand-paused',
|
||||
action: () => {
|
||||
transferQueueDialog.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchRedo'),
|
||||
icon: 'mdi-redo-variant',
|
||||
action: () => {
|
||||
retransferBatch()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchDelete'),
|
||||
icon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
action: () => {
|
||||
removeHistoryBatch()
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
useDynamicButton({
|
||||
icon: historyDynamicIcon,
|
||||
onClick: () => {
|
||||
transferQueueDialog.value = true
|
||||
},
|
||||
)
|
||||
menuItems: historyDynamicMenuItems,
|
||||
show: computed(() => appMode.value),
|
||||
})
|
||||
|
||||
// 初始加载数据
|
||||
onMounted(() => {
|
||||
loadStorages()
|
||||
fetchData()
|
||||
|
||||
// 仅在 Docker 模式下注册动态按钮
|
||||
if (appMode) {
|
||||
registerHistoryDynamicButton(selected.value.length > 0)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAiRedoProgress()
|
||||
if (appMode) {
|
||||
unregisterHistoryDynamicButton()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -754,7 +720,6 @@ onUnmounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="4" md="6" class="text-end">
|
||||
<VBtnGroup variant="outlined" divided rounded>
|
||||
<VBtn icon="mdi-timer-sand-paused" @click="transferQueueDialog = true" />
|
||||
<VBtn :icon="group ? 'mdi-format-list-bulleted' : 'mdi-format-list-group'" @click="group = !group" />
|
||||
</VBtnGroup>
|
||||
</VCol>
|
||||
@@ -1013,10 +978,33 @@ onUnmounted(() => {
|
||||
<!-- 整理队列进度弹窗 -->
|
||||
<TransferQueueDialog v-if="transferQueueDialog" v-model="transferQueueDialog" @close="transferQueueDialog = false" />
|
||||
|
||||
<!-- 非 Docker 模式下的 FAB 按钮 -->
|
||||
<!-- 非 app 模式下的 FAB 按钮 -->
|
||||
<Teleport to="body" v-if="!appMode && route.path === '/history'">
|
||||
<div v-if="isRefreshed && selected.length > 0">
|
||||
<div v-if="isRefreshed">
|
||||
<VFab
|
||||
icon="mdi-timer-sand-paused"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="transferQueueDialog = true"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-16"
|
||||
icon="mdi-redo-variant"
|
||||
color="primary"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom"
|
||||
@@ -1025,17 +1013,7 @@ onUnmounted(() => {
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
class="mb-16"
|
||||
/>
|
||||
<VFab
|
||||
class="mb-32"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -6,21 +6,13 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
|
||||
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -185,6 +177,10 @@ function historyDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function openHistoryDialog() {
|
||||
historyDialog.value = true
|
||||
}
|
||||
|
||||
// 批量管理相关函数
|
||||
// 切换批量模式
|
||||
function toggleBatchMode() {
|
||||
@@ -381,12 +377,8 @@ onActivated(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 使用动态按钮钩子
|
||||
useDynamicButton({
|
||||
icon: 'mdi-history',
|
||||
onClick: () => {
|
||||
historyDialog.value = true
|
||||
},
|
||||
defineExpose({
|
||||
openHistoryDialog,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -477,23 +469,6 @@ useDynamicButton({
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<!-- 底部操作按钮 -->
|
||||
<Teleport to="body" v-if="route.path.startsWith(`/subscribe/${props.type === '电影' ? 'movie' : 'tv'}`)">
|
||||
<div v-if="isRefreshed">
|
||||
<VFab
|
||||
v-if="userStore.superUser && !appMode"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="historyDialog = true"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- 历史记录弹窗 -->
|
||||
<SubscribeHistoryDialog
|
||||
v-if="historyDialog"
|
||||
|
||||
@@ -5,8 +5,6 @@ import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue
|
||||
import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -14,12 +12,6 @@ const { t } = useI18n()
|
||||
// 是否刷新
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 新增对话框
|
||||
const addDialog = ref(false)
|
||||
|
||||
@@ -54,14 +46,6 @@ function addDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 使用动态按钮钩子 新增
|
||||
useDynamicButton({
|
||||
icon: 'mdi-plus',
|
||||
onClick: () => {
|
||||
addDialog.value = true
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadEventTypes()
|
||||
fetchData()
|
||||
@@ -70,6 +54,14 @@ onMounted(() => {
|
||||
onActivated(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
function openAddDialog() {
|
||||
addDialog.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openAddDialog,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
@@ -83,20 +75,6 @@ onActivated(() => {
|
||||
:error-title="t('workflow.noWorkflow')"
|
||||
:error-description="t('workflow.noWorkflowDescription')"
|
||||
/>
|
||||
<!-- 新增按钮 -->
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ const page = ref(1)
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref(props.keyword)
|
||||
const currentKey = ref(0)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
@@ -53,6 +54,17 @@ async function loadEventTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.keyword,
|
||||
newKeyword => {
|
||||
keyword.value = newKeyword || ''
|
||||
page.value = 1
|
||||
dataList.value = []
|
||||
isRefreshed.value = false
|
||||
currentKey.value++
|
||||
},
|
||||
)
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
@@ -141,7 +153,7 @@ onActivated(() => {
|
||||
<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">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData" :key="currentKey">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-workflow-share-card" tabindex="0">
|
||||
|
||||
Reference in New Issue
Block a user