mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-27 11:10:12 +08:00
feat: add subscription sort options
This commit is contained in:
@@ -995,6 +995,13 @@ export default {
|
|||||||
paused: 'Paused',
|
paused: 'Paused',
|
||||||
cardStatePaused: 'Paused',
|
cardStatePaused: 'Paused',
|
||||||
cardStatePending: 'Pending',
|
cardStatePending: 'Pending',
|
||||||
|
sortTitle: 'Sort',
|
||||||
|
sort: {
|
||||||
|
custom: 'Custom',
|
||||||
|
lastUpdate: 'Last Updated',
|
||||||
|
addTime: 'Added Time',
|
||||||
|
lackEpisode: 'Missing Episodes',
|
||||||
|
},
|
||||||
selectedCount: 'Selected {count}/{total} items',
|
selectedCount: 'Selected {count}/{total} items',
|
||||||
noSelectedItems: 'Please select subscriptions to operate',
|
noSelectedItems: 'Please select subscriptions to operate',
|
||||||
batchEnable: 'Batch Enable',
|
batchEnable: 'Batch Enable',
|
||||||
|
|||||||
@@ -990,6 +990,13 @@ export default {
|
|||||||
paused: '暂停',
|
paused: '暂停',
|
||||||
cardStatePaused: '已暂停',
|
cardStatePaused: '已暂停',
|
||||||
cardStatePending: '待定中',
|
cardStatePending: '待定中',
|
||||||
|
sortTitle: '排序',
|
||||||
|
sort: {
|
||||||
|
custom: '自定义',
|
||||||
|
lastUpdate: '最后更新时间',
|
||||||
|
addTime: '添加时间',
|
||||||
|
lackEpisode: '缺失集数',
|
||||||
|
},
|
||||||
selectedCount: '已选择 {count}/{total} 项',
|
selectedCount: '已选择 {count}/{total} 项',
|
||||||
noSelectedItems: '请先选择要操作的订阅',
|
noSelectedItems: '请先选择要操作的订阅',
|
||||||
batchEnable: '批量启用',
|
batchEnable: '批量启用',
|
||||||
|
|||||||
@@ -990,6 +990,13 @@ export default {
|
|||||||
paused: '暫停',
|
paused: '暫停',
|
||||||
cardStatePaused: '已暫停',
|
cardStatePaused: '已暫停',
|
||||||
cardStatePending: '待定中',
|
cardStatePending: '待定中',
|
||||||
|
sortTitle: '排序',
|
||||||
|
sort: {
|
||||||
|
custom: '自定義',
|
||||||
|
lastUpdate: '最後更新時間',
|
||||||
|
addTime: '添加時間',
|
||||||
|
lackEpisode: '缺失集數',
|
||||||
|
},
|
||||||
selectedCount: '已選擇 {count}/{total} 項',
|
selectedCount: '已選擇 {count}/{total} 項',
|
||||||
noSelectedItems: '請先選擇要操作的訂閱',
|
noSelectedItems: '請先選擇要操作的訂閱',
|
||||||
batchEnable: '批量啟用',
|
batchEnable: '批量啟用',
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ const subscribeFilter = ref('')
|
|||||||
// 订阅状态筛选
|
// 订阅状态筛选
|
||||||
const subscribeStatusFilter = ref<string | null>(null)
|
const subscribeStatusFilter = ref<string | null>(null)
|
||||||
|
|
||||||
|
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
|
||||||
|
|
||||||
|
// 订阅排序方式
|
||||||
|
const subscribeSortBy = ref<SubscribeSortBy | ''>('')
|
||||||
|
|
||||||
// 分享搜索词
|
// 分享搜索词
|
||||||
const shareKeyword = ref('')
|
const shareKeyword = ref('')
|
||||||
const shareKeywordInput = ref('')
|
const shareKeywordInput = ref('')
|
||||||
@@ -85,6 +90,17 @@ const filterOptions = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 排序选项
|
||||||
|
const sortOptions = computed<Array<{ value: SubscribeSortBy; label: string }>>(() => [
|
||||||
|
{ value: 'custom', label: t('subscribe.sort.custom') },
|
||||||
|
{ value: 'last_update', label: t('subscribe.sort.lastUpdate') },
|
||||||
|
{ value: 'date', label: t('subscribe.sort.addTime') },
|
||||||
|
{ value: 'lack_episode', label: t('subscribe.sort.lackEpisode') },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 当前选中的排序选项
|
||||||
|
const currentSortBy = computed<SubscribeSortBy>(() => subscribeSortBy.value || 'date')
|
||||||
|
|
||||||
// 当前选中的筛选选项
|
// 当前选中的筛选选项
|
||||||
const currentFilter = computed(() => {
|
const currentFilter = computed(() => {
|
||||||
return filterOptions.value.find(option => option.value === (subscribeStatusFilter.value || 'all'))
|
return filterOptions.value.find(option => option.value === (subscribeStatusFilter.value || 'all'))
|
||||||
@@ -104,6 +120,14 @@ function selectFilter(value: string) {
|
|||||||
filterSubscribeDialog.value = false
|
filterSubscribeDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 选择订阅排序选项,非自定义排序会退出拖拽排序模式。
|
||||||
|
function selectSubscribeSort(value: SubscribeSortBy) {
|
||||||
|
subscribeSortBy.value = value
|
||||||
|
if (value !== 'custom') {
|
||||||
|
subscribeSortMode.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// VMenu activator选择器
|
// VMenu activator选择器
|
||||||
const filterActivator = computed(() => '[data-menu-activator="filter-btn"]')
|
const filterActivator = computed(() => '[data-menu-activator="filter-btn"]')
|
||||||
const searchActivator = computed(() => '[data-menu-activator="share-filter-btn"]')
|
const searchActivator = computed(() => '[data-menu-activator="share-filter-btn"]')
|
||||||
@@ -132,7 +156,11 @@ function openShareStatisticsDialog() {
|
|||||||
openSharedDialog(SubscribeShareStatisticsDialog, {}, {}, { closeOn: ['close'] })
|
openSharedDialog(SubscribeShareStatisticsDialog, {}, {}, { closeOn: ['close'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换订阅拖拽排序模式,进入时固定使用自定义排序。
|
||||||
function toggleSubscribeSortMode() {
|
function toggleSubscribeSortMode() {
|
||||||
|
if (!subscribeSortMode.value) {
|
||||||
|
subscribeSortBy.value = 'custom'
|
||||||
|
}
|
||||||
subscribeSortMode.value = !subscribeSortMode.value
|
subscribeSortMode.value = !subscribeSortMode.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,8 +318,10 @@ onMounted(() => {
|
|||||||
:keyword="subscribeFilter"
|
:keyword="subscribeFilter"
|
||||||
:status-filter="subscribeStatusFilter ?? ''"
|
:status-filter="subscribeStatusFilter ?? ''"
|
||||||
:sort-mode="subscribeSortMode"
|
:sort-mode="subscribeSortMode"
|
||||||
|
:sort-by="subscribeSortBy"
|
||||||
:active="activeTab === 'mysub'"
|
:active="activeTab === 'mysub'"
|
||||||
@update:sort-mode="subscribeSortMode = $event"
|
@update:sort-mode="subscribeSortMode = $event"
|
||||||
|
@update:sort-by="subscribeSortBy = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -358,6 +388,23 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
|
<VDivider />
|
||||||
|
<!-- 排序 -->
|
||||||
|
<VList density="compact" class="px-2 py-1">
|
||||||
|
<VListSubheader>{{ t('subscribe.sortTitle') }}</VListSubheader>
|
||||||
|
<VListItem
|
||||||
|
v-for="option in sortOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:active="currentSortBy === option.value"
|
||||||
|
@click="selectSubscribeSort(option.value)"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
<VListItemTitle>{{ option.label }}</VListItemTitle>
|
||||||
|
<template #append>
|
||||||
|
<VIcon v-if="currentSortBy === option.value" icon="mdi-check" color="primary" size="small" />
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
sortBy: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
active: {
|
active: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -48,8 +52,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:sortMode': [value: boolean]
|
'update:sortMode': [value: boolean]
|
||||||
|
'update:sortBy': [value: SubscribeSortBy]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
let isRefreshed = ref(false)
|
let isRefreshed = ref(false)
|
||||||
|
|
||||||
@@ -71,8 +78,16 @@ 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 effectiveSortBy = computed<SubscribeSortBy>(() => {
|
||||||
|
return (props.sortBy as SubscribeSortBy) || (hasCustomOrder.value ? 'custom' : 'date')
|
||||||
|
})
|
||||||
const canSortContext = computed(
|
const canSortContext = computed(
|
||||||
() => !normalizedKeyword.value && (!props.statusFilter || props.statusFilter === 'all') && !isBatchMode.value,
|
() =>
|
||||||
|
effectiveSortBy.value === 'custom' &&
|
||||||
|
!normalizedKeyword.value &&
|
||||||
|
(!props.statusFilter || props.statusFilter === 'all') &&
|
||||||
|
!isBatchMode.value,
|
||||||
)
|
)
|
||||||
const sortMode = computed({
|
const sortMode = computed({
|
||||||
get: () => props.sortMode,
|
get: () => props.sortMode,
|
||||||
@@ -130,11 +145,62 @@ function getSubscribeStatus(subscribe: Subscribe) {
|
|||||||
// API请求键值(计算属性)
|
// API请求键值(计算属性)
|
||||||
const orderRequestKey = computed(() => (props.type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'))
|
const orderRequestKey = computed(() => (props.type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'))
|
||||||
|
|
||||||
// 监听数据和筛选变化,同步更新显示列表
|
// 转换订阅时间字段为可排序时间戳。
|
||||||
|
function getSubscribeTimeValue(value?: string) {
|
||||||
|
if (!value) return 0
|
||||||
|
|
||||||
|
const directTime = Date.parse(value)
|
||||||
|
if (!Number.isNaN(directTime)) return directTime
|
||||||
|
|
||||||
|
const compatibleTime = Date.parse(value.replace(/-/g, '/'))
|
||||||
|
return Number.isNaN(compatibleTime) ? 0 : compatibleTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按自定义顺序排序订阅,未配置顺序的订阅按添加时间倒序补齐。
|
||||||
|
function sortByCustomOrder(a: Subscribe, b: Subscribe, orderIndexMap: Map<number, number>) {
|
||||||
|
const aIndex = orderIndexMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
|
||||||
|
const bIndex = orderIndexMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
|
if (aIndex !== bIndex) {
|
||||||
|
return aIndex - bIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSubscribeTimeValue(b.date) - getSubscribeTimeValue(a.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按当前排序选项调整订阅列表顺序。
|
||||||
|
function sortSubscribeList(list: Subscribe[]) {
|
||||||
|
const orderIndexMap = new Map(orderConfig.value.map((item, index) => [item.id, index]))
|
||||||
|
|
||||||
|
list.sort((a, b) => {
|
||||||
|
if (effectiveSortBy.value === 'custom') {
|
||||||
|
return sortByCustomOrder(a, b, orderIndexMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveSortBy.value === 'last_update') {
|
||||||
|
return getSubscribeTimeValue(b.last_update) - getSubscribeTimeValue(a.last_update)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveSortBy.value === 'lack_episode') {
|
||||||
|
const lackEpisodeDiff = (b.lack_episode || 0) - (a.lack_episode || 0)
|
||||||
|
return lackEpisodeDiff || getSubscribeTimeValue(b.date) - getSubscribeTimeValue(a.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSubscribeTimeValue(b.date) - getSubscribeTimeValue(a.date)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步订阅排序默认值给父组件。
|
||||||
|
function syncDefaultSortBy() {
|
||||||
|
if (!props.sortBy) {
|
||||||
|
emit('update:sortBy', hasCustomOrder.value ? 'custom' : 'date')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据、筛选和排序变化,同步更新显示列表
|
||||||
watch(
|
watch(
|
||||||
[dataList, normalizedKeyword, () => props.statusFilter, orderConfig],
|
[dataList, normalizedKeyword, () => props.statusFilter, orderConfig, effectiveSortBy],
|
||||||
() => {
|
() => {
|
||||||
const orderIndexMap = new Map(orderConfig.value.map((item, index) => [item.id, index]))
|
|
||||||
const nextDisplayList = dataList.value.filter(data => {
|
const nextDisplayList = dataList.value.filter(data => {
|
||||||
if (data.type !== props.type) {
|
if (data.type !== props.type) {
|
||||||
return false
|
return false
|
||||||
@@ -155,12 +221,7 @@ watch(
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
nextDisplayList.sort((a, b) => {
|
sortSubscribeList(nextDisplayList)
|
||||||
const aIndex = orderIndexMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
|
|
||||||
const bIndex = orderIndexMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
|
|
||||||
|
|
||||||
return aIndex - bIndex
|
|
||||||
})
|
|
||||||
|
|
||||||
displayList.value = nextDisplayList
|
displayList.value = nextDisplayList
|
||||||
},
|
},
|
||||||
@@ -184,9 +245,11 @@ async function loadSubscribeOrderConfig() {
|
|||||||
if (response && response.data && response.data.value) {
|
if (response && response.data && response.data.value) {
|
||||||
orderConfig.value = response.data.value
|
orderConfig.value = response.data.value
|
||||||
}
|
}
|
||||||
|
syncDefaultSortBy()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load subscribe order config:', error)
|
console.error('Failed to load subscribe order config:', error)
|
||||||
orderConfig.value = []
|
orderConfig.value = []
|
||||||
|
syncDefaultSortBy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +258,7 @@ async function saveSubscribeOrder() {
|
|||||||
// 顺序配置
|
// 顺序配置
|
||||||
const orderObj = displayList.value.map(item => ({ id: item.id }))
|
const orderObj = displayList.value.map(item => ({ id: item.id }))
|
||||||
orderConfig.value = orderObj
|
orderConfig.value = orderObj
|
||||||
|
emit('update:sortBy', 'custom')
|
||||||
|
|
||||||
// 保存到服务端
|
// 保存到服务端
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user