mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 18:10:49 +08:00
实现订阅批量管理功能
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -19,6 +19,5 @@ declare module 'vue' {
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default']
|
||||
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
|
||||
VDialog: typeof import('./src/@core/components/VDialog.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,14 @@ const { t } = useI18n()
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<Subscribe>,
|
||||
batchMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
@@ -29,7 +37,7 @@ const globalSettingsStore = useGlobalSettingsStore()
|
||||
const globalSettings = globalSettingsStore.globalSettings
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
const emit = defineEmits(['remove', 'save', 'select'])
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
@@ -297,6 +305,17 @@ function onSubscribeEditRemove() {
|
||||
subscribeEditDialog.value = false
|
||||
emit('remove')
|
||||
}
|
||||
|
||||
// 处理卡片点击事件
|
||||
function handleCardClick() {
|
||||
if (props.batchMode) {
|
||||
// 批量模式下触发选择事件
|
||||
emit('select')
|
||||
} else {
|
||||
// 非批量模式下打开编辑弹窗
|
||||
editSubscribeDialog()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -308,6 +327,7 @@ function onSubscribeEditRemove() {
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
|
||||
}"
|
||||
>
|
||||
<VCard
|
||||
@@ -319,8 +339,8 @@ function onSubscribeEditRemove() {
|
||||
}"
|
||||
rounded="0"
|
||||
min-height="150"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
@click="handleCardClick"
|
||||
:ripple="!props.batchMode"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-4">
|
||||
<IconBtn>
|
||||
|
||||
@@ -833,6 +833,23 @@ export default {
|
||||
notStarted: 'Not Started',
|
||||
pending: 'Pending',
|
||||
paused: 'Paused',
|
||||
selectedCount: 'Selected {count}/{total} items',
|
||||
noSelectedItems: 'Please select subscriptions to operate',
|
||||
batchEnable: 'Batch Enable',
|
||||
batchPause: 'Batch Pause',
|
||||
batchDelete: 'Batch Delete',
|
||||
batchEnableConfirm: 'Are you sure you want to enable {count} selected subscriptions?',
|
||||
batchPauseConfirm: 'Are you sure you want to pause {count} selected subscriptions?',
|
||||
batchDeleteConfirm: 'Are you sure you want to delete {count} selected subscriptions? This action cannot be undone!',
|
||||
batchEnableSuccess: 'Successfully enabled {count} subscriptions',
|
||||
batchPauseSuccess: 'Successfully paused {count} subscriptions',
|
||||
batchDeleteSuccess: 'Successfully deleted {count} subscriptions',
|
||||
batchEnableFailed: 'Failed to enable {count} subscriptions',
|
||||
batchPauseFailed: 'Failed to pause {count} subscriptions',
|
||||
batchDeleteFailed: 'Failed to delete {count} subscriptions',
|
||||
batchEnableError: 'Batch enable operation failed',
|
||||
batchPauseError: 'Batch pause operation failed',
|
||||
batchDeleteError: 'Batch delete operation failed',
|
||||
},
|
||||
recommend: {
|
||||
all: 'All',
|
||||
|
||||
@@ -829,6 +829,23 @@ export default {
|
||||
notStarted: '未开始',
|
||||
pending: '待定',
|
||||
paused: '暂停',
|
||||
selectedCount: '已选择 {count}/{total} 项',
|
||||
noSelectedItems: '请先选择要操作的订阅',
|
||||
batchEnable: '批量启用',
|
||||
batchPause: '批量暂停',
|
||||
batchDelete: '批量删除',
|
||||
batchEnableConfirm: '确定要启用选中的 {count} 个订阅吗?',
|
||||
batchPauseConfirm: '确定要暂停选中的 {count} 个订阅吗?',
|
||||
batchDeleteConfirm: '确定要删除选中的 {count} 个订阅吗?此操作不可恢复!',
|
||||
batchEnableSuccess: '成功启用 {count} 个订阅',
|
||||
batchPauseSuccess: '成功暂停 {count} 个订阅',
|
||||
batchDeleteSuccess: '成功删除 {count} 个订阅',
|
||||
batchEnableFailed: '启用失败 {count} 个订阅',
|
||||
batchPauseFailed: '暂停失败 {count} 个订阅',
|
||||
batchDeleteFailed: '删除失败 {count} 个订阅',
|
||||
batchEnableError: '批量启用操作失败',
|
||||
batchPauseError: '批量暂停操作失败',
|
||||
batchDeleteError: '批量删除操作失败',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
|
||||
@@ -827,6 +827,23 @@ export default {
|
||||
notStarted: '未開始',
|
||||
pending: '待定',
|
||||
paused: '暫停',
|
||||
selectedCount: '已選擇 {count}/{total} 項',
|
||||
noSelectedItems: '請先選擇要操作的訂閱',
|
||||
batchEnable: '批量啟用',
|
||||
batchPause: '批量暫停',
|
||||
batchDelete: '批量刪除',
|
||||
batchEnableConfirm: '確定要啟用選中的 {count} 個訂閱嗎?',
|
||||
batchPauseConfirm: '確定要暫停選中的 {count} 個訂閱嗎?',
|
||||
batchDeleteConfirm: '確定要刪除選中的 {count} 個訂閱嗎?此操作不可恢復!',
|
||||
batchEnableSuccess: '成功啟用 {count} 個訂閱',
|
||||
batchPauseSuccess: '成功暫停 {count} 個訂閱',
|
||||
batchDeleteSuccess: '成功刪除 {count} 個訂閱',
|
||||
batchEnableFailed: '啟用失敗 {count} 個訂閱',
|
||||
batchPauseFailed: '暫停失敗 {count} 個訂閱',
|
||||
batchDeleteFailed: '刪除失敗 {count} 個訂閱',
|
||||
batchEnableError: '批量啟用操作失敗',
|
||||
batchPauseError: '批量暫停操作失敗',
|
||||
batchDeleteError: '批量刪除操作失敗',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
|
||||
@@ -113,6 +113,18 @@ registerHeaderTab({
|
||||
},
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-checkbox-multiple-marked-outline',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
class: 'settings-icon-button',
|
||||
action: () => {
|
||||
// 触发批量管理模式
|
||||
const event = new CustomEvent('toggle-batch-mode')
|
||||
window.dispatchEvent(event)
|
||||
},
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-chart-line',
|
||||
variant: 'text',
|
||||
|
||||
@@ -9,6 +9,8 @@ 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()
|
||||
@@ -22,6 +24,12 @@ const { appMode } = usePWA()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 从 Store 中获取用户信息
|
||||
const superUser = userStore.superUser
|
||||
const userName = userStore.userName
|
||||
@@ -52,6 +60,10 @@ const orderConfig = ref<{ id: number }[]>([])
|
||||
// 显示的订阅列表
|
||||
const displayList = ref<Subscribe[]>([])
|
||||
|
||||
// 批量管理相关状态
|
||||
const isBatchMode = ref(false)
|
||||
const selectedSubscribes = ref<number[]>([])
|
||||
|
||||
// 根据订阅数据判断订阅状态
|
||||
function getSubscribeStatus(subscribe: Subscribe) {
|
||||
// 洗版中
|
||||
@@ -173,6 +185,160 @@ function historyDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 批量管理相关函数
|
||||
// 切换批量模式
|
||||
function toggleBatchMode() {
|
||||
isBatchMode.value = !isBatchMode.value
|
||||
if (!isBatchMode.value) {
|
||||
selectedSubscribes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
function toggleSelectAll() {
|
||||
if (selectedSubscribes.value.length === displayList.value.length) {
|
||||
selectedSubscribes.value = []
|
||||
} else {
|
||||
selectedSubscribes.value = displayList.value.map(item => item.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择单个订阅
|
||||
function toggleSelectSubscribe(id: number) {
|
||||
const index = selectedSubscribes.value.indexOf(id)
|
||||
if (index > -1) {
|
||||
selectedSubscribes.value.splice(index, 1)
|
||||
} else {
|
||||
selectedSubscribes.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除订阅
|
||||
async function batchDeleteSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchDeleteConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.delete(`subscribe/${id}`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchDeleteSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchDeleteFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchDeleteError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量启用订阅
|
||||
async function batchEnableSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchEnableConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.put(`subscribe/status/${id}?state=R`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchEnableSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchEnableFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchEnableError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量暂停订阅
|
||||
async function batchPauseSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchPauseConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.put(`subscribe/status/${id}?state=S`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchPauseSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchPauseFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchPauseError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 错误描述
|
||||
const errorDescription = computed(() => {
|
||||
if ((props.statusFilter && props.statusFilter !== 'all') || props.keyword) {
|
||||
@@ -199,6 +365,14 @@ onMounted(async () => {
|
||||
sub.page_open = true
|
||||
}
|
||||
}
|
||||
|
||||
// 监听批量管理模式切换事件
|
||||
window.addEventListener('toggle-batch-mode', toggleBatchMode)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('toggle-batch-mode', toggleBatchMode)
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
@@ -218,6 +392,63 @@ useDynamicButton({
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
|
||||
<!-- 批量管理工具栏 -->
|
||||
<div v-if="isBatchMode" class="mb-4 px-2">
|
||||
<VCard class="pa-4">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<VCheckbox
|
||||
:model-value="selectedSubscribes.length === displayList.length"
|
||||
:indeterminate="selectedSubscribes.length > 0 && selectedSubscribes.length < displayList.length"
|
||||
@update:model-value="toggleSelectAll"
|
||||
hide-details
|
||||
class="me-4"
|
||||
/>
|
||||
<span class="text-body-1 font-weight-medium">
|
||||
{{ t('subscribe.selectedCount', { count: selectedSubscribes.length, total: displayList.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchEnableSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-play" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchEnable') }}</span>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="info"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchPauseSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-pause" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchPause') }}</span>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchDeleteSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-delete" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchDelete') }}</span>
|
||||
</VBtn>
|
||||
<VBtn color="secondary" variant="outlined" size="small" @click="toggleBatchMode">
|
||||
<VIcon icon="mdi-close" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('common.cancel') }}</span>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-if="displayList.length > 0"
|
||||
v-model="displayList"
|
||||
@@ -226,10 +457,18 @@ useDynamicButton({
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ class: 'grid gap-4 grid-subscribe-card px-2' }"
|
||||
:disabled="props.keyword || (props.statusFilter && props.statusFilter !== 'all')"
|
||||
:disabled="props.keyword || (props.statusFilter && props.statusFilter !== 'all') || isBatchMode"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
|
||||
<SubscribeCard
|
||||
:key="element.id"
|
||||
:media="element"
|
||||
:batch-mode="isBatchMode"
|
||||
:selected="selectedSubscribes.includes(element.id)"
|
||||
@remove="fetchData"
|
||||
@save="fetchData"
|
||||
@select="toggleSelectSubscribe(element.id)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
<NoDataFound
|
||||
|
||||
Reference in New Issue
Block a user