feat: add subscription sort options

This commit is contained in:
jxxghp
2026-05-25 11:29:35 +08:00
parent abbce2644a
commit 6da0aae362
5 changed files with 142 additions and 10 deletions

View File

@@ -995,6 +995,13 @@ export default {
paused: 'Paused',
cardStatePaused: 'Paused',
cardStatePending: 'Pending',
sortTitle: 'Sort',
sort: {
custom: 'Custom',
lastUpdate: 'Last Updated',
addTime: 'Added Time',
lackEpisode: 'Missing Episodes',
},
selectedCount: 'Selected {count}/{total} items',
noSelectedItems: 'Please select subscriptions to operate',
batchEnable: 'Batch Enable',

View File

@@ -990,6 +990,13 @@ export default {
paused: '暂停',
cardStatePaused: '已暂停',
cardStatePending: '待定中',
sortTitle: '排序',
sort: {
custom: '自定义',
lastUpdate: '最后更新时间',
addTime: '添加时间',
lackEpisode: '缺失集数',
},
selectedCount: '已选择 {count}/{total} 项',
noSelectedItems: '请先选择要操作的订阅',
batchEnable: '批量启用',

View File

@@ -990,6 +990,13 @@ export default {
paused: '暫停',
cardStatePaused: '已暫停',
cardStatePending: '待定中',
sortTitle: '排序',
sort: {
custom: '自定義',
lastUpdate: '最後更新時間',
addTime: '添加時間',
lackEpisode: '缺失集數',
},
selectedCount: '已選擇 {count}/{total} 項',
noSelectedItems: '請先選擇要操作的訂閱',
batchEnable: '批量啟用',

View File

@@ -54,6 +54,11 @@ const subscribeFilter = ref('')
// 订阅状态筛选
const subscribeStatusFilter = ref<string | null>(null)
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
// 订阅排序方式
const subscribeSortBy = ref<SubscribeSortBy | ''>('')
// 分享搜索词
const shareKeyword = 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(() => {
return filterOptions.value.find(option => option.value === (subscribeStatusFilter.value || 'all'))
@@ -104,6 +120,14 @@ function selectFilter(value: string) {
filterSubscribeDialog.value = false
}
// 选择订阅排序选项,非自定义排序会退出拖拽排序模式。
function selectSubscribeSort(value: SubscribeSortBy) {
subscribeSortBy.value = value
if (value !== 'custom') {
subscribeSortMode.value = false
}
}
// VMenu activator选择器
const filterActivator = computed(() => '[data-menu-activator="filter-btn"]')
const searchActivator = computed(() => '[data-menu-activator="share-filter-btn"]')
@@ -132,7 +156,11 @@ function openShareStatisticsDialog() {
openSharedDialog(SubscribeShareStatisticsDialog, {}, {}, { closeOn: ['close'] })
}
// 切换订阅拖拽排序模式,进入时固定使用自定义排序。
function toggleSubscribeSortMode() {
if (!subscribeSortMode.value) {
subscribeSortBy.value = 'custom'
}
subscribeSortMode.value = !subscribeSortMode.value
}
@@ -290,8 +318,10 @@ onMounted(() => {
:keyword="subscribeFilter"
:status-filter="subscribeStatusFilter ?? ''"
:sort-mode="subscribeSortMode"
:sort-by="subscribeSortBy"
:active="activeTab === 'mysub'"
@update:sort-mode="subscribeSortMode = $event"
@update:sort-by="subscribeSortBy = $event"
/>
</div>
</transition>
@@ -358,6 +388,23 @@ onMounted(() => {
</template>
</VListItem>
</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>
</VMenu>
</Teleport>

View File

@@ -40,6 +40,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
sortBy: {
type: String,
default: '',
},
active: {
type: Boolean,
default: true,
@@ -48,8 +52,11 @@ const props = defineProps({
const emit = defineEmits<{
'update:sortMode': [value: boolean]
'update:sortBy': [value: SubscribeSortBy]
}>()
type SubscribeSortBy = 'custom' | 'last_update' | 'date' | 'lack_episode'
// 是否刷新过
let isRefreshed = ref(false)
@@ -71,8 +78,16 @@ const selectedSubscribes = ref<number[]>([])
const normalizedKeyword = computed(() => props.keyword?.trim().toLowerCase() || '')
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(
() => !normalizedKeyword.value && (!props.statusFilter || props.statusFilter === 'all') && !isBatchMode.value,
() =>
effectiveSortBy.value === 'custom' &&
!normalizedKeyword.value &&
(!props.statusFilter || props.statusFilter === 'all') &&
!isBatchMode.value,
)
const sortMode = computed({
get: () => props.sortMode,
@@ -130,11 +145,62 @@ function getSubscribeStatus(subscribe: Subscribe) {
// API请求键值计算属性
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(
[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 => {
if (data.type !== props.type) {
return false
@@ -155,12 +221,7 @@ watch(
return true
})
nextDisplayList.sort((a, b) => {
const aIndex = orderIndexMap.get(a.id) ?? Number.MAX_SAFE_INTEGER
const bIndex = orderIndexMap.get(b.id) ?? Number.MAX_SAFE_INTEGER
return aIndex - bIndex
})
sortSubscribeList(nextDisplayList)
displayList.value = nextDisplayList
},
@@ -184,9 +245,11 @@ async function loadSubscribeOrderConfig() {
if (response && response.data && response.data.value) {
orderConfig.value = response.data.value
}
syncDefaultSortBy()
} catch (error) {
console.error('Failed to load subscribe order config:', error)
orderConfig.value = []
syncDefaultSortBy()
}
}
@@ -195,6 +258,7 @@ async function saveSubscribeOrder() {
// 顺序配置
const orderObj = displayList.value.map(item => ({ id: item.id }))
orderConfig.value = orderObj
emit('update:sortBy', 'custom')
// 保存到服务端
try {