mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
Add batch selection support for subscriptions and history
This commit is contained in:
@@ -31,6 +31,7 @@ export interface DynamicButtonMenuItem {
|
|||||||
icon?: string
|
icon?: string
|
||||||
color?: string
|
color?: string
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
disabled?: boolean
|
||||||
action: () => void
|
action: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ const legacyDynamicMenuTitleKeyMap: Record<string, string> = {
|
|||||||
'components.pluginMarketSetting.title': 'dialog.pluginMarketSetting.title',
|
'components.pluginMarketSetting.title': 'dialog.pluginMarketSetting.title',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析动态按钮菜单项标题,兼容旧版直接传入 i18n key 的写法。
|
||||||
function resolveDynamicMenuItemTitle(item: DynamicButtonMenuItem) {
|
function resolveDynamicMenuItemTitle(item: DynamicButtonMenuItem) {
|
||||||
if (item.titleKey) {
|
if (item.titleKey) {
|
||||||
return t(item.titleKey, item.titleParams as any)
|
return t(item.titleKey, item.titleParams as any)
|
||||||
@@ -202,14 +203,16 @@ function resolveDynamicMenuItemTitle(item: DynamicButtonMenuItem) {
|
|||||||
return looksLikeI18nKey ? t(normalizedTitleKey, item.titleParams as any) : item.title
|
return looksLikeI18nKey ? t(normalizedTitleKey, item.titleParams as any) : item.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理页面注册的动态按钮主操作点击。
|
||||||
function handleDynamicButtonClick() {
|
function handleDynamicButtonClick() {
|
||||||
if (!dynamicButton.value || !hasItemPermission(dynamicButton.value, userPermissions.value)) return
|
if (!dynamicButton.value || !hasItemPermission(dynamicButton.value, userPermissions.value)) return
|
||||||
|
|
||||||
dynamicButton.value.action()
|
dynamicButton.value.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理页面注册的动态按钮菜单项点击。
|
||||||
function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||||
if (!hasItemPermission(item, userPermissions.value)) return
|
if (item.disabled || !hasItemPermission(item, userPermissions.value)) return
|
||||||
|
|
||||||
item.action()
|
item.action()
|
||||||
}
|
}
|
||||||
@@ -292,6 +295,7 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
|||||||
v-for="(item, index) in visibleDynamicButtonMenuItems"
|
v-for="(item, index) in visibleDynamicButtonMenuItems"
|
||||||
:key="item.titleKey || item.title || index"
|
:key="item.titleKey || item.title || index"
|
||||||
:base-color="item.color"
|
:base-color="item.color"
|
||||||
|
:disabled="item.disabled"
|
||||||
@click="handleDynamicMenuItemClick(item)"
|
@click="handleDynamicMenuItemClick(item)"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
|||||||
@@ -1105,6 +1105,9 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedCount: 'Selected {count}/{total} items',
|
selectedCount: 'Selected {count}/{total} items',
|
||||||
noSelectedItems: 'Please select subscriptions to operate',
|
noSelectedItems: 'Please select subscriptions to operate',
|
||||||
|
batchSelectAll: 'Select All Subscriptions',
|
||||||
|
batchDeselectAll: 'Deselect All Subscriptions',
|
||||||
|
exitBatchMode: 'Exit Batch Mode',
|
||||||
batchEnable: 'Batch Enable',
|
batchEnable: 'Batch Enable',
|
||||||
batchPause: 'Batch Pause',
|
batchPause: 'Batch Pause',
|
||||||
batchDelete: 'Batch Delete',
|
batchDelete: 'Batch Delete',
|
||||||
@@ -3290,6 +3293,7 @@ export default {
|
|||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
pageSize: 'Items Per Page',
|
pageSize: 'Items Per Page',
|
||||||
pageInfo: '{begin} - {end} / {total}',
|
pageInfo: '{begin} - {end} / {total}',
|
||||||
|
selectedCount: 'Selected {count}/{total} items',
|
||||||
aiRedoDisabled: 'Please enable the AI assistant in system settings first',
|
aiRedoDisabled: 'Please enable the AI assistant in system settings first',
|
||||||
aiRedoQueued: 'Assistant organize task submitted: {title}',
|
aiRedoQueued: 'Assistant organize task submitted: {title}',
|
||||||
aiRedoFailed: 'Failed to submit assistant organize task',
|
aiRedoFailed: 'Failed to submit assistant organize task',
|
||||||
@@ -3297,6 +3301,11 @@ export default {
|
|||||||
aiRedo: 'Assistant Organize',
|
aiRedo: 'Assistant Organize',
|
||||||
aiRedoPending: 'Assistant Organizing...',
|
aiRedoPending: 'Assistant Organizing...',
|
||||||
batchAiRedo: 'Assistant Batch Organize',
|
batchAiRedo: 'Assistant Batch Organize',
|
||||||
|
batchSelect: 'Batch Select',
|
||||||
|
exitBatchSelect: 'Exit Batch Select',
|
||||||
|
exitBatchMode: 'Exit Batch Mode',
|
||||||
|
selectAll: 'Select All',
|
||||||
|
deselectAll: 'Deselect All',
|
||||||
redo: 'Reorganize',
|
redo: 'Reorganize',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
batchRedo: 'Batch Reorganize',
|
batchRedo: 'Batch Reorganize',
|
||||||
|
|||||||
@@ -1100,6 +1100,9 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedCount: '已选择 {count}/{total} 项',
|
selectedCount: '已选择 {count}/{total} 项',
|
||||||
noSelectedItems: '请先选择要操作的订阅',
|
noSelectedItems: '请先选择要操作的订阅',
|
||||||
|
batchSelectAll: '选择全部订阅',
|
||||||
|
batchDeselectAll: '取消全选订阅',
|
||||||
|
exitBatchMode: '退出批量操作',
|
||||||
batchEnable: '批量启用',
|
batchEnable: '批量启用',
|
||||||
batchPause: '批量暂停',
|
batchPause: '批量暂停',
|
||||||
batchDelete: '批量删除',
|
batchDelete: '批量删除',
|
||||||
@@ -3233,6 +3236,7 @@ export default {
|
|||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
pageSize: '每页条数',
|
pageSize: '每页条数',
|
||||||
pageInfo: '{begin} - {end} / {total}',
|
pageInfo: '{begin} - {end} / {total}',
|
||||||
|
selectedCount: '已选择 {count}/{total} 项',
|
||||||
aiRedoDisabled: '请先在系统设置中启用 AI 智能助手',
|
aiRedoDisabled: '请先在系统设置中启用 AI 智能助手',
|
||||||
aiRedoQueued: '已提交智能助手整理任务:{title}',
|
aiRedoQueued: '已提交智能助手整理任务:{title}',
|
||||||
aiRedoFailed: '提交智能助手整理任务失败',
|
aiRedoFailed: '提交智能助手整理任务失败',
|
||||||
@@ -3240,6 +3244,11 @@ export default {
|
|||||||
aiRedo: '智能助手整理',
|
aiRedo: '智能助手整理',
|
||||||
aiRedoPending: '智能助手整理中...',
|
aiRedoPending: '智能助手整理中...',
|
||||||
batchAiRedo: '智能助手批量整理',
|
batchAiRedo: '智能助手批量整理',
|
||||||
|
batchSelect: '批量选择',
|
||||||
|
exitBatchSelect: '退出批量选择',
|
||||||
|
exitBatchMode: '退出批量操作',
|
||||||
|
selectAll: '全部选中',
|
||||||
|
deselectAll: '取消全选',
|
||||||
redo: '重新整理',
|
redo: '重新整理',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
batchRedo: '批量重新整理',
|
batchRedo: '批量重新整理',
|
||||||
|
|||||||
@@ -1100,6 +1100,9 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedCount: '已選擇 {count}/{total} 項',
|
selectedCount: '已選擇 {count}/{total} 項',
|
||||||
noSelectedItems: '請先選擇要操作的訂閱',
|
noSelectedItems: '請先選擇要操作的訂閱',
|
||||||
|
batchSelectAll: '選擇全部訂閱',
|
||||||
|
batchDeselectAll: '取消全選訂閱',
|
||||||
|
exitBatchMode: '退出批量操作',
|
||||||
batchEnable: '批量啟用',
|
batchEnable: '批量啟用',
|
||||||
batchPause: '批量暫停',
|
batchPause: '批量暫停',
|
||||||
batchDelete: '批量刪除',
|
batchDelete: '批量刪除',
|
||||||
@@ -3232,6 +3235,7 @@ export default {
|
|||||||
loading: '加載中...',
|
loading: '加載中...',
|
||||||
pageSize: '每頁條數',
|
pageSize: '每頁條數',
|
||||||
pageInfo: '{begin} - {end} / {total}',
|
pageInfo: '{begin} - {end} / {total}',
|
||||||
|
selectedCount: '已選擇 {count}/{total} 項',
|
||||||
aiRedoDisabled: '請先在系統設置中啟用 AI 智能助手',
|
aiRedoDisabled: '請先在系統設置中啟用 AI 智能助手',
|
||||||
aiRedoQueued: '已提交智能助手整理任務:{title}',
|
aiRedoQueued: '已提交智能助手整理任務:{title}',
|
||||||
aiRedoFailed: '提交智能助手整理任務失敗',
|
aiRedoFailed: '提交智能助手整理任務失敗',
|
||||||
@@ -3239,6 +3243,11 @@ export default {
|
|||||||
aiRedo: '智能助手整理',
|
aiRedo: '智能助手整理',
|
||||||
aiRedoPending: '智能助手整理中...',
|
aiRedoPending: '智能助手整理中...',
|
||||||
batchAiRedo: '智能助手批量整理',
|
batchAiRedo: '智能助手批量整理',
|
||||||
|
batchSelect: '批量選擇',
|
||||||
|
exitBatchSelect: '退出批量選擇',
|
||||||
|
exitBatchMode: '退出批量操作',
|
||||||
|
selectAll: '全部選中',
|
||||||
|
deselectAll: '取消全選',
|
||||||
redo: '重新整理',
|
redo: '重新整理',
|
||||||
delete: '刪除',
|
delete: '刪除',
|
||||||
batchRedo: '批量重新整理',
|
batchRedo: '批量重新整理',
|
||||||
|
|||||||
+194
-17
@@ -3,7 +3,7 @@ import { debounce } from 'lodash-es'
|
|||||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||||
@@ -31,6 +31,21 @@ const subId = ref(route.query.id as string)
|
|||||||
const activeTab = ref((route.query.tab as string) || '')
|
const activeTab = ref((route.query.tab as string) || '')
|
||||||
const subscribeListViewRef = ref<InstanceType<typeof SubscribeListView> | null>(null)
|
const subscribeListViewRef = ref<InstanceType<typeof SubscribeListView> | null>(null)
|
||||||
|
|
||||||
|
// 订阅批量模式状态快照,来源于订阅列表组件。
|
||||||
|
interface SubscribeBatchState {
|
||||||
|
enabled: boolean
|
||||||
|
selectedCount: number
|
||||||
|
totalCount: number
|
||||||
|
allSelected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeBatchState = ref<SubscribeBatchState>({
|
||||||
|
enabled: false,
|
||||||
|
selectedCount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
allSelected: false,
|
||||||
|
})
|
||||||
|
|
||||||
// 获取标签页
|
// 获取标签页
|
||||||
const subscribeTabs = computed(() => {
|
const subscribeTabs = computed(() => {
|
||||||
if (subType === '电影') {
|
if (subType === '电影') {
|
||||||
@@ -221,6 +236,52 @@ function openShareStatisticsDialog() {
|
|||||||
openSharedDialog(SubscribeShareStatisticsDialog, {}, {}, { closeOn: ['close'] })
|
openSharedDialog(SubscribeShareStatisticsDialog, {}, {}, { closeOn: ['close'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅列表批量状态变化响应,用于驱动移动端 Footer 和桌面 FAB 操作按钮。
|
||||||
|
function handleSubscribeBatchStateChange(state: SubscribeBatchState) {
|
||||||
|
subscribeBatchState.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置父页面保存的订阅批量操作状态。
|
||||||
|
function resetSubscribeBatchState() {
|
||||||
|
subscribeBatchState.value = {
|
||||||
|
enabled: false,
|
||||||
|
selectedCount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
allSelected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入订阅批量操作模式。
|
||||||
|
function enterSubscribeBatchMode() {
|
||||||
|
subscribeListViewRef.value?.enterBatchMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出订阅批量操作模式。
|
||||||
|
function exitSubscribeBatchMode() {
|
||||||
|
resetSubscribeBatchState()
|
||||||
|
subscribeListViewRef.value?.exitBatchMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换当前订阅列表全选状态。
|
||||||
|
function toggleSubscribeBatchSelectAll() {
|
||||||
|
subscribeListViewRef.value?.toggleSelectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量启用已选订阅。
|
||||||
|
function batchEnableSelectedSubscribes() {
|
||||||
|
subscribeListViewRef.value?.batchEnableSubscribes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量暂停已选订阅。
|
||||||
|
function batchPauseSelectedSubscribes() {
|
||||||
|
subscribeListViewRef.value?.batchPauseSubscribes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除已选订阅。
|
||||||
|
function batchDeleteSelectedSubscribes() {
|
||||||
|
subscribeListViewRef.value?.batchDeleteSubscribes()
|
||||||
|
}
|
||||||
|
|
||||||
// 切换订阅拖拽排序模式,进入时固定使用自定义排序。
|
// 切换订阅拖拽排序模式,进入时固定使用自定义排序。
|
||||||
function toggleSubscribeSortMode() {
|
function toggleSubscribeSortMode() {
|
||||||
if (!subscribeSortMode.value) {
|
if (!subscribeSortMode.value) {
|
||||||
@@ -241,6 +302,10 @@ watch(activeTab, newTab => {
|
|||||||
if (newTab !== 'share') {
|
if (newTab !== 'share') {
|
||||||
searchShareDialog.value = false
|
searchShareDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newTab !== 'mysub' && subscribeBatchState.value.enabled) {
|
||||||
|
exitSubscribeBatchMode()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(subscribeSortBy, newSortBy => {
|
watch(subscribeSortBy, newSortBy => {
|
||||||
@@ -251,17 +316,68 @@ onUnmounted(() => {
|
|||||||
shareKeywordUpdater.cancel()
|
shareKeywordUpdater.cancel()
|
||||||
})
|
})
|
||||||
|
|
||||||
const subscribeDynamicMenuItems = computed(() => {
|
const subscribeDynamicMenuItems = computed<DynamicButtonMenuItem[] | undefined>(() => {
|
||||||
if (!appMode.value) return undefined
|
if (!appMode.value) return undefined
|
||||||
|
|
||||||
if (activeTab.value === 'mysub') {
|
if (activeTab.value === 'mysub') {
|
||||||
const items: Array<{
|
if (subscribeBatchState.value.enabled) {
|
||||||
titleKey: string
|
const hasSelectedSubscribes = subscribeBatchState.value.selectedCount > 0
|
||||||
titleParams?: Record<string, unknown>
|
|
||||||
icon: string
|
return [
|
||||||
permission: 'admin'
|
{
|
||||||
action: () => void
|
titleKey: 'subscribe.selectedCount',
|
||||||
}> = []
|
titleParams: {
|
||||||
|
count: subscribeBatchState.value.selectedCount,
|
||||||
|
total: subscribeBatchState.value.totalCount,
|
||||||
|
},
|
||||||
|
icon: 'mdi-checkbox-multiple-marked-outline',
|
||||||
|
permission: 'subscribe',
|
||||||
|
disabled: true,
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: subscribeBatchState.value.allSelected
|
||||||
|
? 'subscribe.batchDeselectAll'
|
||||||
|
: 'subscribe.batchSelectAll',
|
||||||
|
icon: subscribeBatchState.value.allSelected ? 'mdi-checkbox-blank-outline' : 'mdi-checkbox-multiple-marked',
|
||||||
|
permission: 'subscribe',
|
||||||
|
disabled: subscribeBatchState.value.totalCount === 0,
|
||||||
|
action: toggleSubscribeBatchSelectAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'subscribe.batchEnable',
|
||||||
|
icon: 'mdi-play',
|
||||||
|
color: 'success',
|
||||||
|
permission: 'subscribe',
|
||||||
|
disabled: !hasSelectedSubscribes,
|
||||||
|
action: batchEnableSelectedSubscribes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'subscribe.batchPause',
|
||||||
|
icon: 'mdi-pause',
|
||||||
|
color: 'info',
|
||||||
|
permission: 'subscribe',
|
||||||
|
disabled: !hasSelectedSubscribes,
|
||||||
|
action: batchPauseSelectedSubscribes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'subscribe.batchDelete',
|
||||||
|
icon: 'mdi-delete',
|
||||||
|
color: 'error',
|
||||||
|
permission: 'subscribe',
|
||||||
|
disabled: !hasSelectedSubscribes,
|
||||||
|
action: batchDeleteSelectedSubscribes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'subscribe.exitBatchMode',
|
||||||
|
icon: 'mdi-close',
|
||||||
|
permission: 'subscribe',
|
||||||
|
action: exitSubscribeBatchMode,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: DynamicButtonMenuItem[] = []
|
||||||
|
|
||||||
if (showSubscribeHistoryAction.value) {
|
if (showSubscribeHistoryAction.value) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -287,12 +403,18 @@ const subscribeDynamicMenuItems = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const subscribeDynamicIcon = computed(() => {
|
const subscribeDynamicIcon = computed(() => {
|
||||||
|
if (subscribeBatchState.value.enabled) return 'mdi-checkbox-multiple-marked-outline'
|
||||||
if (showShareStatisticsAction.value) return 'mdi-chart-line'
|
if (showShareStatisticsAction.value) return 'mdi-chart-line'
|
||||||
if (showSubscribeHistoryAction.value) return 'mdi-history'
|
if (showSubscribeHistoryAction.value) return 'mdi-history'
|
||||||
return 'mdi-clipboard-edit-outline'
|
return 'mdi-clipboard-edit-outline'
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSubscribeDynamicAction() {
|
function handleSubscribeDynamicAction() {
|
||||||
|
if (subscribeBatchState.value.enabled) {
|
||||||
|
exitSubscribeBatchMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (showShareStatisticsAction.value) {
|
if (showShareStatisticsAction.value) {
|
||||||
openShareStatisticsDialog()
|
openShareStatisticsDialog()
|
||||||
return
|
return
|
||||||
@@ -313,7 +435,9 @@ useDynamicButton({
|
|||||||
onClick: handleSubscribeDynamicAction,
|
onClick: handleSubscribeDynamicAction,
|
||||||
menuItems: subscribeDynamicMenuItems,
|
menuItems: subscribeDynamicMenuItems,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
show: computed(() => appMode.value && (showDefaultRuleAction.value || showShareStatisticsAction.value)),
|
show: computed(
|
||||||
|
() => appMode.value && (subscribeBatchState.value.enabled || showDefaultRuleAction.value || showShareStatisticsAction.value),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 使用动态标签页
|
// 使用动态标签页
|
||||||
@@ -348,13 +472,16 @@ registerHeaderTab({
|
|||||||
{
|
{
|
||||||
icon: 'mdi-checkbox-multiple-marked-outline',
|
icon: 'mdi-checkbox-multiple-marked-outline',
|
||||||
variant: 'text',
|
variant: 'text',
|
||||||
color: 'gray',
|
color: computed(() => (subscribeBatchState.value.enabled ? 'primary' : 'gray')),
|
||||||
class: 'settings-icon-button',
|
class: 'settings-icon-button',
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
action: () => {
|
action: () => {
|
||||||
// 触发批量管理模式
|
if (subscribeBatchState.value.enabled) {
|
||||||
const event = new CustomEvent('toggle-batch-mode')
|
exitSubscribeBatchMode()
|
||||||
window.dispatchEvent(event)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enterSubscribeBatchMode()
|
||||||
},
|
},
|
||||||
show: computed(() => activeTab.value === 'mysub'),
|
show: computed(() => activeTab.value === 'mysub'),
|
||||||
},
|
},
|
||||||
@@ -399,6 +526,7 @@ onMounted(() => {
|
|||||||
:active="activeTab === 'mysub'"
|
:active="activeTab === 'mysub'"
|
||||||
@update:sort-mode="subscribeSortMode = $event"
|
@update:sort-mode="subscribeSortMode = $event"
|
||||||
@update:sort-by="subscribeSortBy = $event"
|
@update:sort-by="subscribeSortBy = $event"
|
||||||
|
@batch-state-change="handleSubscribeBatchStateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -513,7 +641,56 @@ onMounted(() => {
|
|||||||
<Teleport to="body" v-if="!appMode && route.path.startsWith(`/subscribe/${subType === '电影' ? 'movie' : 'tv'}`)">
|
<Teleport to="body" v-if="!appMode && route.path.startsWith(`/subscribe/${subType === '电影' ? 'movie' : 'tv'}`)">
|
||||||
<div class="compact-fab-stack">
|
<div class="compact-fab-stack">
|
||||||
<VFab
|
<VFab
|
||||||
v-if="showSubscribeHistoryAction"
|
v-if="subscribeBatchState.enabled"
|
||||||
|
icon="mdi-close"
|
||||||
|
color="secondary"
|
||||||
|
variant="tonal"
|
||||||
|
appear
|
||||||
|
class="compact-fab compact-fab--secondary"
|
||||||
|
@click="exitSubscribeBatchMode"
|
||||||
|
/>
|
||||||
|
<VFab
|
||||||
|
v-if="subscribeBatchState.enabled"
|
||||||
|
:icon="subscribeBatchState.allSelected ? 'mdi-checkbox-blank-outline' : 'mdi-checkbox-multiple-marked'"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
appear
|
||||||
|
class="compact-fab compact-fab--secondary"
|
||||||
|
:disabled="subscribeBatchState.totalCount === 0"
|
||||||
|
@click="toggleSubscribeBatchSelectAll"
|
||||||
|
/>
|
||||||
|
<VFab
|
||||||
|
v-if="subscribeBatchState.enabled"
|
||||||
|
icon="mdi-delete"
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
appear
|
||||||
|
class="compact-fab compact-fab--secondary"
|
||||||
|
:disabled="subscribeBatchState.selectedCount === 0"
|
||||||
|
@click="batchDeleteSelectedSubscribes"
|
||||||
|
/>
|
||||||
|
<VFab
|
||||||
|
v-if="subscribeBatchState.enabled"
|
||||||
|
icon="mdi-pause"
|
||||||
|
color="info"
|
||||||
|
variant="tonal"
|
||||||
|
appear
|
||||||
|
class="compact-fab compact-fab--secondary"
|
||||||
|
:disabled="subscribeBatchState.selectedCount === 0"
|
||||||
|
@click="batchPauseSelectedSubscribes"
|
||||||
|
/>
|
||||||
|
<VFab
|
||||||
|
v-if="subscribeBatchState.enabled"
|
||||||
|
icon="mdi-play"
|
||||||
|
color="success"
|
||||||
|
variant="tonal"
|
||||||
|
appear
|
||||||
|
class="compact-fab compact-fab--secondary"
|
||||||
|
:disabled="subscribeBatchState.selectedCount === 0"
|
||||||
|
@click="batchEnableSelectedSubscribes"
|
||||||
|
/>
|
||||||
|
<VFab
|
||||||
|
v-if="!subscribeBatchState.enabled && showSubscribeHistoryAction"
|
||||||
icon="mdi-history"
|
icon="mdi-history"
|
||||||
color="info"
|
color="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@@ -522,7 +699,7 @@ onMounted(() => {
|
|||||||
@click="openSubscribeHistoryDialog"
|
@click="openSubscribeHistoryDialog"
|
||||||
/>
|
/>
|
||||||
<VFab
|
<VFab
|
||||||
v-if="showDefaultRuleAction"
|
v-if="!subscribeBatchState.enabled && showDefaultRuleAction"
|
||||||
icon="mdi-clipboard-edit-outline"
|
icon="mdi-clipboard-edit-outline"
|
||||||
color="primary"
|
color="primary"
|
||||||
appear
|
appear
|
||||||
@@ -530,7 +707,7 @@ onMounted(() => {
|
|||||||
@click="openDefaultRuleDialog"
|
@click="openDefaultRuleDialog"
|
||||||
/>
|
/>
|
||||||
<VFab
|
<VFab
|
||||||
v-if="showShareStatisticsAction"
|
v-if="!subscribeBatchState.enabled && showShareStatisticsAction"
|
||||||
icon="mdi-chart-line"
|
icon="mdi-chart-line"
|
||||||
color="primary"
|
color="primary"
|
||||||
appear
|
appear
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ const redoTargetStorage = ref<string>()
|
|||||||
// 已选中的数据
|
// 已选中的数据
|
||||||
const selected = ref<TransferHistory[]>([])
|
const selected = ref<TransferHistory[]>([])
|
||||||
|
|
||||||
|
// 移动端批量选择模式
|
||||||
|
const mobileBatchMode = ref(false)
|
||||||
|
|
||||||
// 从季集字符串中提取可排序的数字。
|
// 从季集字符串中提取可排序的数字。
|
||||||
const getNum = (s?: string) => (s ? parseInt(s.replace(/[^0-9]/g, ''), 10) || 0 : 0)
|
const getNum = (s?: string) => (s ? parseInt(s.replace(/[^0-9]/g, ''), 10) || 0 : 0)
|
||||||
|
|
||||||
@@ -504,6 +507,7 @@ function resetMobileHistory() {
|
|||||||
totalItems.value = 0
|
totalItems.value = 0
|
||||||
mobileExpandedPathIds.value = []
|
mobileExpandedPathIds.value = []
|
||||||
selected.value = []
|
selected.value = []
|
||||||
|
mobileBatchMode.value = false
|
||||||
mobileInfiniteKey.value++
|
mobileInfiniteKey.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +703,9 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
|||||||
}
|
}
|
||||||
// 清空选中项
|
// 清空选中项
|
||||||
selected.value = []
|
selected.value = []
|
||||||
|
if (isMobile.value) {
|
||||||
|
mobileBatchMode.value = false
|
||||||
|
}
|
||||||
// 隐藏进度条
|
// 隐藏进度条
|
||||||
closeProgressDialog()
|
closeProgressDialog()
|
||||||
// 重新获取数据
|
// 重新获取数据
|
||||||
@@ -742,6 +749,9 @@ async function transferDone() {
|
|||||||
// 清空当前操作记录
|
// 清空当前操作记录
|
||||||
currentHistory.value = undefined
|
currentHistory.value = undefined
|
||||||
selected.value = []
|
selected.value = []
|
||||||
|
if (isMobile.value) {
|
||||||
|
mobileBatchMode.value = false
|
||||||
|
}
|
||||||
// 刷新
|
// 刷新
|
||||||
await refreshDataAfterOperation()
|
await refreshDataAfterOperation()
|
||||||
}
|
}
|
||||||
@@ -886,6 +896,9 @@ async function triggerBatchAiRedo() {
|
|||||||
}
|
}
|
||||||
startAiRedoProgressBatch(acceptedIds, progressKey)
|
startAiRedoProgressBatch(acceptedIds, progressKey)
|
||||||
selected.value = selected.value.filter(item => !acceptedIds.includes(item.id))
|
selected.value = selected.value.filter(item => !acceptedIds.includes(item.id))
|
||||||
|
if (isMobile.value && selected.value.length === 0) {
|
||||||
|
mobileBatchMode.value = false
|
||||||
|
}
|
||||||
progressStarted = true
|
progressStarted = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -984,6 +997,19 @@ function ensurePageSize(value: any, defaultValue: number = 50) {
|
|||||||
// 已选历史记录 ID 集合,供移动端卡片和分组选择状态复用。
|
// 已选历史记录 ID 集合,供移动端卡片和分组选择状态复用。
|
||||||
const selectedIdSet = computed(() => new Set(selected.value.map(item => item.id)))
|
const selectedIdSet = computed(() => new Set(selected.value.map(item => item.id)))
|
||||||
|
|
||||||
|
// 移动端当前已加载记录数量,用于批量菜单展示选择进度。
|
||||||
|
const mobileBatchTotalCount = computed(() => mobileDataList.value.length)
|
||||||
|
|
||||||
|
// 移动端当前已加载记录中的已选数量。
|
||||||
|
const mobileBatchSelectedCount = computed(() => {
|
||||||
|
return mobileDataList.value.filter(item => selectedIdSet.value.has(item.id)).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移动端当前已加载记录是否已全部选中。
|
||||||
|
const isAllMobileHistorySelected = computed(() => {
|
||||||
|
return mobileBatchTotalCount.value > 0 && mobileBatchSelectedCount.value === mobileBatchTotalCount.value
|
||||||
|
})
|
||||||
|
|
||||||
// 拼接移动端展示用的季集文本。
|
// 拼接移动端展示用的季集文本。
|
||||||
function getHistoryEpisodeText(item: TransferHistory) {
|
function getHistoryEpisodeText(item: TransferHistory) {
|
||||||
return `${item.seasons || ''}${item.episodes || ''}`
|
return `${item.seasons || ''}${item.episodes || ''}`
|
||||||
@@ -1085,6 +1111,50 @@ function toggleHistorySelection(item: TransferHistory, checked: boolean | null)
|
|||||||
updateHistorySelection([item], checked)
|
updateHistorySelection([item], checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换移动端批量选择模式,退出时清空移动端选择状态。
|
||||||
|
function toggleMobileBatchMode() {
|
||||||
|
if (mobileBatchMode.value) {
|
||||||
|
exitMobileBatchMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.value = []
|
||||||
|
mobileBatchMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出移动端批量选择模式并清空选择状态。
|
||||||
|
function exitMobileBatchMode() {
|
||||||
|
mobileBatchMode.value = false
|
||||||
|
selected.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量模式下点击移动端记录卡片时切换该记录的选择状态。
|
||||||
|
function handleMobileRecordClick(item: TransferHistory) {
|
||||||
|
if (!mobileBatchMode.value) return
|
||||||
|
|
||||||
|
toggleHistorySelection(item, !isHistorySelected(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端路径点击在批量模式下转为选择记录,普通模式下展开路径。
|
||||||
|
function handleMobilePathClick(item: TransferHistory) {
|
||||||
|
if (mobileBatchMode.value) {
|
||||||
|
handleMobileRecordClick(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMobilePathExpanded(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中移动端当前已加载的全部历史记录。
|
||||||
|
function selectAllMobileHistory() {
|
||||||
|
updateHistorySelection(mobileDataList.value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消移动端历史记录的全部选择。
|
||||||
|
function deselectAllMobileHistory() {
|
||||||
|
updateHistorySelection(mobileDataList.value, false)
|
||||||
|
}
|
||||||
|
|
||||||
// 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数
|
// 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数
|
||||||
const selectedCountsGroupedByTitle = computed(() => {
|
const selectedCountsGroupedByTitle = computed(() => {
|
||||||
return selected.value.reduce(
|
return selected.value.reduce(
|
||||||
@@ -1103,14 +1173,95 @@ const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) =>
|
|||||||
updateHistorySelection(values, checked)
|
updateHistorySelection(values, checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyDynamicIcon = computed(() => (selected.value.length > 0 ? 'mdi-chevron-up' : 'mdi-timer-sand-paused'))
|
const historyDynamicIcon = computed(() => 'mdi-timer-sand-paused')
|
||||||
const historyDynamicMenuItems = computed(() => {
|
const historyDynamicMenuItems = computed(() => {
|
||||||
|
if (!appMode.value) return undefined
|
||||||
|
|
||||||
|
if (mobileBatchMode.value) {
|
||||||
|
const hasSelectedHistory = mobileBatchSelectedCount.value > 0
|
||||||
|
|
||||||
|
const items: DynamicButtonMenuItem[] = [
|
||||||
|
{
|
||||||
|
titleKey: 'transferHistory.selectedCount',
|
||||||
|
titleParams: {
|
||||||
|
count: mobileBatchSelectedCount.value,
|
||||||
|
total: mobileBatchTotalCount.value,
|
||||||
|
},
|
||||||
|
icon: 'mdi-checkbox-multiple-marked-outline',
|
||||||
|
permission: 'manage',
|
||||||
|
disabled: true,
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: isAllMobileHistorySelected.value
|
||||||
|
? 'transferHistory.actions.deselectAll'
|
||||||
|
: 'transferHistory.actions.selectAll',
|
||||||
|
icon: isAllMobileHistorySelected.value ? 'mdi-checkbox-blank-outline' : 'mdi-checkbox-multiple-marked',
|
||||||
|
permission: 'manage',
|
||||||
|
disabled: mobileBatchTotalCount.value === 0,
|
||||||
|
action: () => {
|
||||||
|
if (isAllMobileHistorySelected.value) {
|
||||||
|
deselectAllMobileHistory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAllMobileHistory()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!hasRunningAiRedo.value) {
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
titleKey: 'transferHistory.actions.batchAiRedo',
|
||||||
|
icon: 'mdi-robot-outline',
|
||||||
|
color: 'info',
|
||||||
|
permission: 'manage',
|
||||||
|
disabled: !hasSelectedHistory,
|
||||||
|
action: () => {
|
||||||
|
triggerBatchAiRedo()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'transferHistory.actions.batchRedo',
|
||||||
|
icon: 'mdi-redo-variant',
|
||||||
|
color: 'success',
|
||||||
|
permission: 'manage',
|
||||||
|
disabled: !hasSelectedHistory,
|
||||||
|
action: () => {
|
||||||
|
retransferBatch()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'transferHistory.actions.batchDelete',
|
||||||
|
icon: 'mdi-trash-can-outline',
|
||||||
|
color: 'error',
|
||||||
|
permission: 'manage',
|
||||||
|
disabled: !hasSelectedHistory,
|
||||||
|
action: () => {
|
||||||
|
removeHistoryBatch()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
titleKey: 'transferHistory.actions.exitBatchMode',
|
||||||
|
icon: 'mdi-close',
|
||||||
|
permission: 'manage',
|
||||||
|
action: exitMobileBatchMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
if (selected.value.length === 0) return undefined
|
if (selected.value.length === 0) return undefined
|
||||||
|
|
||||||
const items: DynamicButtonMenuItem[] = [
|
const items: DynamicButtonMenuItem[] = [
|
||||||
{
|
{
|
||||||
titleKey: 'dialog.transferQueue.title',
|
titleKey: 'dialog.transferQueue.title',
|
||||||
icon: 'mdi-timer-sand-paused',
|
icon: 'mdi-timer-sand-paused',
|
||||||
|
color: 'primary',
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
action: openTransferQueueDialog,
|
action: openTransferQueueDialog,
|
||||||
},
|
},
|
||||||
@@ -1121,6 +1272,7 @@ const historyDynamicMenuItems = computed(() => {
|
|||||||
{
|
{
|
||||||
titleKey: 'transferHistory.actions.batchAiRedo',
|
titleKey: 'transferHistory.actions.batchAiRedo',
|
||||||
icon: 'mdi-robot-outline',
|
icon: 'mdi-robot-outline',
|
||||||
|
color: 'info',
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
action: () => {
|
action: () => {
|
||||||
triggerBatchAiRedo()
|
triggerBatchAiRedo()
|
||||||
@@ -1129,6 +1281,7 @@ const historyDynamicMenuItems = computed(() => {
|
|||||||
{
|
{
|
||||||
titleKey: 'transferHistory.actions.batchRedo',
|
titleKey: 'transferHistory.actions.batchRedo',
|
||||||
icon: 'mdi-redo-variant',
|
icon: 'mdi-redo-variant',
|
||||||
|
color: 'success',
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
action: () => {
|
action: () => {
|
||||||
retransferBatch()
|
retransferBatch()
|
||||||
@@ -1442,7 +1595,23 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<section v-else class="transfer-history-mobile-page">
|
<section v-else class="transfer-history-mobile-page">
|
||||||
<div class="transfer-history-mobile-titlebar">
|
<div class="transfer-history-mobile-titlebar">
|
||||||
<VPageContentTitle :title="t('navItems.mediaOrganize')" class="transfer-history-mobile-title my-0" style="margin-block: 0" />
|
<VPageContentTitle
|
||||||
|
:title="t('navItems.mediaOrganize')"
|
||||||
|
class="transfer-history-mobile-title my-0"
|
||||||
|
style="margin-block: 0"
|
||||||
|
/>
|
||||||
|
<VBtn
|
||||||
|
v-if="canManage"
|
||||||
|
icon="mdi-checkbox-multiple-marked-outline"
|
||||||
|
:color="mobileBatchMode ? 'primary' : 'gray'"
|
||||||
|
:aria-label="
|
||||||
|
mobileBatchMode ? t('transferHistory.actions.exitBatchSelect') : t('transferHistory.actions.batchSelect')
|
||||||
|
"
|
||||||
|
:title="mobileBatchMode ? t('transferHistory.actions.exitBatchSelect') : t('transferHistory.actions.batchSelect')"
|
||||||
|
variant="text"
|
||||||
|
class="settings-icon-button transfer-history-mobile-titlebar__batch"
|
||||||
|
@click="toggleMobileBatchMode"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VCombobox
|
<VCombobox
|
||||||
@@ -1491,9 +1660,11 @@ onUnmounted(() => {
|
|||||||
:ref="itemRef"
|
:ref="itemRef"
|
||||||
class="transfer-history-mobile-record"
|
class="transfer-history-mobile-record"
|
||||||
:class="{
|
:class="{
|
||||||
'transfer-history-mobile-record--selected': isHistorySelected(item),
|
'transfer-history-mobile-record--batch': mobileBatchMode,
|
||||||
|
'transfer-history-mobile-record--selected': mobileBatchMode && isHistorySelected(item),
|
||||||
'transfer-history-mobile-record--failed': !item.status,
|
'transfer-history-mobile-record--failed': !item.status,
|
||||||
}"
|
}"
|
||||||
|
@click="handleMobileRecordClick(item)"
|
||||||
>
|
>
|
||||||
<header class="transfer-history-mobile-record__header">
|
<header class="transfer-history-mobile-record__header">
|
||||||
<VAvatar class="transfer-history-mobile-record__avatar" size="40">
|
<VAvatar class="transfer-history-mobile-record__avatar" size="40">
|
||||||
@@ -1513,7 +1684,17 @@ onUnmounted(() => {
|
|||||||
{{ getHistoryStatusText(item) }}
|
{{ getHistoryStatusText(item) }}
|
||||||
</VChip>
|
</VChip>
|
||||||
|
|
||||||
<IconBtn class="transfer-history-mobile-record__menu" size="small">
|
<VCheckbox
|
||||||
|
v-if="mobileBatchMode"
|
||||||
|
class="transfer-history-mobile-record__checkbox"
|
||||||
|
:model-value="isHistorySelected(item)"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
@click.stop
|
||||||
|
@update:model-value="checked => toggleHistorySelection(item, checked)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconBtn v-else class="transfer-history-mobile-record__menu" size="small">
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VList>
|
<VList>
|
||||||
@@ -1547,7 +1728,7 @@ onUnmounted(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
class="transfer-history-mobile-record__paths"
|
class="transfer-history-mobile-record__paths"
|
||||||
:class="{ 'transfer-history-mobile-record__paths--expanded': isMobilePathExpanded(item) }"
|
:class="{ 'transfer-history-mobile-record__paths--expanded': isMobilePathExpanded(item) }"
|
||||||
@click="toggleMobilePathExpanded(item)"
|
@click.stop="handleMobilePathClick(item)"
|
||||||
>
|
>
|
||||||
<div class="transfer-history-mobile-record__path-row">
|
<div class="transfer-history-mobile-record__path-row">
|
||||||
<span class="transfer-history-mobile-record__storage">
|
<span class="transfer-history-mobile-record__storage">
|
||||||
@@ -1582,7 +1763,7 @@ onUnmounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 非 app 模式下的 FAB 按钮 -->
|
<!-- 非 app 模式下的 FAB 按钮 -->
|
||||||
<Teleport to="body" v-if="!appMode && route.path === '/history' && isDesktop">
|
<Teleport to="body" v-if="!appMode && route.path === '/history'">
|
||||||
<div v-if="isRefreshed && canManage" class="compact-fab-stack compact-fab-stack--history">
|
<div v-if="isRefreshed && canManage" class="compact-fab-stack compact-fab-stack--history">
|
||||||
<VFab
|
<VFab
|
||||||
v-if="selected.length > 0 && !hasRunningAiRedo"
|
v-if="selected.length > 0 && !hasRunningAiRedo"
|
||||||
@@ -1649,12 +1830,19 @@ onUnmounted(() => {
|
|||||||
.transfer-history-mobile-titlebar {
|
.transfer-history-mobile-titlebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-history-mobile-title {
|
.transfer-history-mobile-title {
|
||||||
|
flex: 1;
|
||||||
min-inline-size: 0;
|
min-inline-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-history-mobile-titlebar__batch {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.transfer-history-mobile-title :deep(h2) {
|
.transfer-history-mobile-title :deep(h2) {
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
@@ -1714,6 +1902,15 @@ onUnmounted(() => {
|
|||||||
box-shadow: var(--app-card-rest-shadow);
|
box-shadow: var(--app-card-rest-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-history-mobile-record--batch {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-history-mobile-record--selected {
|
||||||
|
outline: 2px solid rgb(var(--v-theme-primary));
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
.transfer-history-mobile-record + .transfer-history-mobile-record {
|
.transfer-history-mobile-record + .transfer-history-mobile-record {
|
||||||
margin-block-start: 0.875rem;
|
margin-block-start: 0.875rem;
|
||||||
}
|
}
|
||||||
@@ -1770,6 +1967,17 @@ onUnmounted(() => {
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-history-mobile-record__checkbox {
|
||||||
|
align-self: start;
|
||||||
|
justify-self: end;
|
||||||
|
margin-block-start: -0.35rem;
|
||||||
|
margin-inline-end: -0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-history-mobile-record__checkbox :deep(.v-selection-control) {
|
||||||
|
min-block-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.transfer-history-mobile-record__meta {
|
.transfer-history-mobile-record__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -53,10 +53,19 @@ const props = defineProps({
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:sortMode': [value: boolean]
|
'update:sortMode': [value: boolean]
|
||||||
'update:sortBy': [value: SubscribeSortBy]
|
'update:sortBy': [value: SubscribeSortBy]
|
||||||
|
'batch-state-change': [state: SubscribeBatchState]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
|
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
|
||||||
|
|
||||||
|
// 订阅批量模式状态快照,供父页面渲染外部批量操作按钮。
|
||||||
|
interface SubscribeBatchState {
|
||||||
|
enabled: boolean
|
||||||
|
selectedCount: number
|
||||||
|
totalCount: number
|
||||||
|
allSelected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
let isRefreshed = ref(false)
|
let isRefreshed = ref(false)
|
||||||
|
|
||||||
@@ -79,6 +88,9 @@ const selectedSubscribes = ref<number[]>([])
|
|||||||
const normalizedKeyword = computed(() => props.keyword?.trim().toLowerCase() || '')
|
const normalizedKeyword = computed(() => props.keyword?.trim().toLowerCase() || '')
|
||||||
const selectedSubscribesSet = computed(() => new Set(selectedSubscribes.value))
|
const selectedSubscribesSet = computed(() => new Set(selectedSubscribes.value))
|
||||||
const hasCustomOrder = computed(() => orderConfig.value.length > 0)
|
const hasCustomOrder = computed(() => orderConfig.value.length > 0)
|
||||||
|
const isAllSubscribesSelected = computed(
|
||||||
|
() => displayList.value.length > 0 && selectedSubscribes.value.length === displayList.value.length,
|
||||||
|
)
|
||||||
|
|
||||||
// 归一化订阅排序方式,电影订阅不使用缺失集数排序。
|
// 归一化订阅排序方式,电影订阅不使用缺失集数排序。
|
||||||
const normalizedSortBy = computed<SubscribeSortBy | ''>(() => {
|
const normalizedSortBy = computed<SubscribeSortBy | ''>(() => {
|
||||||
@@ -248,6 +260,12 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[isBatchMode, () => selectedSubscribes.value.length, () => displayList.value.length, isAllSubscribesSelected],
|
||||||
|
emitBatchStateChange,
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 加载顺序
|
// 加载顺序
|
||||||
async function loadSubscribeOrderConfig() {
|
async function loadSubscribeOrderConfig() {
|
||||||
try {
|
try {
|
||||||
@@ -313,25 +331,47 @@ function openHistoryDialog() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量管理相关函数
|
// 向父组件同步批量操作状态,供 Footer/FAB 动态按钮渲染。
|
||||||
// 切换批量模式
|
function emitBatchStateChange() {
|
||||||
function toggleBatchMode() {
|
emit('batch-state-change', {
|
||||||
isBatchMode.value = !isBatchMode.value
|
enabled: isBatchMode.value,
|
||||||
if (!isBatchMode.value) {
|
selectedCount: selectedSubscribes.value.length,
|
||||||
selectedSubscribes.value = []
|
totalCount: displayList.value.length,
|
||||||
}
|
allSelected: isAllSubscribesSelected.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全选/取消全选
|
// 进入批量模式。
|
||||||
|
function enterBatchMode() {
|
||||||
|
isBatchMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出批量模式并清空已选择的订阅。
|
||||||
|
function exitBatchMode() {
|
||||||
|
isBatchMode.value = false
|
||||||
|
selectedSubscribes.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换批量模式。
|
||||||
|
function toggleBatchMode() {
|
||||||
|
if (isBatchMode.value) {
|
||||||
|
exitBatchMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enterBatchMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选或取消全选当前显示的订阅。
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
if (selectedSubscribes.value.length === displayList.value.length) {
|
if (isAllSubscribesSelected.value) {
|
||||||
selectedSubscribes.value = []
|
selectedSubscribes.value = []
|
||||||
} else {
|
} else {
|
||||||
selectedSubscribes.value = displayList.value.map(item => item.id)
|
selectedSubscribes.value = displayList.value.map(item => item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择单个订阅
|
// 切换单个订阅的选中状态。
|
||||||
function toggleSelectSubscribe(id: number) {
|
function toggleSelectSubscribe(id: number) {
|
||||||
const index = selectedSubscribes.value.indexOf(id)
|
const index = selectedSubscribes.value.indexOf(id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -341,7 +381,7 @@ function toggleSelectSubscribe(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除订阅
|
// 批量删除已选中的订阅。
|
||||||
async function batchDeleteSubscribes() {
|
async function batchDeleteSubscribes() {
|
||||||
if (selectedSubscribes.value.length === 0) {
|
if (selectedSubscribes.value.length === 0) {
|
||||||
$toast.warning(t('subscribe.noSelectedItems'))
|
$toast.warning(t('subscribe.noSelectedItems'))
|
||||||
@@ -370,11 +410,8 @@ async function batchDeleteSubscribes() {
|
|||||||
$toast.error(t('subscribe.batchDeleteFailed', { count: failedCount }))
|
$toast.error(t('subscribe.batchDeleteFailed', { count: failedCount }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await fetchData()
|
await fetchData()
|
||||||
// 退出批量模式
|
exitBatchMode()
|
||||||
isBatchMode.value = false
|
|
||||||
selectedSubscribes.value = []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$toast.error(t('subscribe.batchDeleteError'))
|
$toast.error(t('subscribe.batchDeleteError'))
|
||||||
@@ -383,7 +420,7 @@ async function batchDeleteSubscribes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量启用订阅
|
// 批量启用已选中的订阅。
|
||||||
async function batchEnableSubscribes() {
|
async function batchEnableSubscribes() {
|
||||||
if (selectedSubscribes.value.length === 0) {
|
if (selectedSubscribes.value.length === 0) {
|
||||||
$toast.warning(t('subscribe.noSelectedItems'))
|
$toast.warning(t('subscribe.noSelectedItems'))
|
||||||
@@ -412,11 +449,8 @@ async function batchEnableSubscribes() {
|
|||||||
$toast.error(t('subscribe.batchEnableFailed', { count: failedCount }))
|
$toast.error(t('subscribe.batchEnableFailed', { count: failedCount }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await fetchData()
|
await fetchData()
|
||||||
// 退出批量模式
|
exitBatchMode()
|
||||||
isBatchMode.value = false
|
|
||||||
selectedSubscribes.value = []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$toast.error(t('subscribe.batchEnableError'))
|
$toast.error(t('subscribe.batchEnableError'))
|
||||||
@@ -425,7 +459,7 @@ async function batchEnableSubscribes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量暂停订阅
|
// 批量暂停已选中的订阅。
|
||||||
async function batchPauseSubscribes() {
|
async function batchPauseSubscribes() {
|
||||||
if (selectedSubscribes.value.length === 0) {
|
if (selectedSubscribes.value.length === 0) {
|
||||||
$toast.warning(t('subscribe.noSelectedItems'))
|
$toast.warning(t('subscribe.noSelectedItems'))
|
||||||
@@ -454,11 +488,8 @@ async function batchPauseSubscribes() {
|
|||||||
$toast.error(t('subscribe.batchPauseFailed', { count: failedCount }))
|
$toast.error(t('subscribe.batchPauseFailed', { count: failedCount }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await fetchData()
|
await fetchData()
|
||||||
// 退出批量模式
|
exitBatchMode()
|
||||||
isBatchMode.value = false
|
|
||||||
selectedSubscribes.value = []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$toast.error(t('subscribe.batchPauseError'))
|
$toast.error(t('subscribe.batchPauseError'))
|
||||||
@@ -495,13 +526,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听批量管理模式切换事件
|
|
||||||
window.addEventListener('toggle-batch-mode', toggleBatchMode)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// 移除事件监听器
|
|
||||||
window.removeEventListener('toggle-batch-mode', toggleBatchMode)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useKeepAliveRefresh(fetchData, {
|
useKeepAliveRefresh(fetchData, {
|
||||||
@@ -510,68 +534,19 @@ useKeepAliveRefresh(fetchData, {
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openHistoryDialog,
|
openHistoryDialog,
|
||||||
|
enterBatchMode,
|
||||||
|
exitBatchMode,
|
||||||
|
toggleBatchMode,
|
||||||
|
toggleSelectAll,
|
||||||
|
batchEnableSubscribes,
|
||||||
|
batchPauseSubscribes,
|
||||||
|
batchDeleteSubscribes,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<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>
|
|
||||||
|
|
||||||
<VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 mx-2 py-0 app-surface-static">
|
<VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 mx-2 py-0 app-surface-static">
|
||||||
<div class="d-flex flex-wrap align-center justify-space-between gap-2 py-5">
|
<div class="d-flex flex-wrap align-center justify-space-between gap-2 py-5">
|
||||||
<span>{{ t('common.sortModeHint') }}</span>
|
<span>{{ t('common.sortModeHint') }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user