feat: 更新 HeaderTab 组件以支持动态项和排序功能

This commit is contained in:
jxxghp
2025-04-09 10:39:17 +08:00
parent f031077fbd
commit fc357a03e5
6 changed files with 208 additions and 96 deletions
+5 -5
View File
@@ -73,14 +73,14 @@ onUnmounted(() => {
<div class="tab-header"> <div class="tab-header">
<div ref="tabsContainerRef" class="header-tabs" :class="{ 'show-indicator': showTabsScrollIndicator }"> <div ref="tabsContainerRef" class="header-tabs" :class="{ 'show-indicator': showTabsScrollIndicator }">
<div <div
v-for="(category, index) in items" v-for="(item, index) in items"
:key="index" :key="index"
class="header-tab" class="header-tab"
:class="{ 'active': currentValue === category.title }" :class="{ 'active': currentValue === item.title }"
@click="currentValue = category.title" @click="currentValue = item.title"
> >
<VIcon :icon="category.icon" size="small" class="header-tab-icon" /> <VIcon v-if="item.icon" :icon="item.icon" size="small" class="header-tab-icon" />
<span>{{ category.title }}</span> <span>{{ item.title }}</span>
</div> </div>
</div> </div>
<slot name="append" /> <slot name="append" />
+50 -55
View File
@@ -347,6 +347,7 @@ onDeactivated(() => {
<VIcon icon="mdi-tune" size="small" class="me-2" /> <VIcon icon="mdi-tune" size="small" class="me-2" />
设置仪表板 设置仪表板
</VCardTitle> </VCardTitle>
<DialogCloseBtn @click="dialog = false" />
</VCardItem> </VCardItem>
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -385,7 +386,6 @@ onDeactivated(() => {
<VDivider /> <VDivider />
<VCardText class="pt-5 text-end"> <VCardText class="pt-5 text-end">
<VSpacer /> <VSpacer />
<VBtn variant="outlined" color="secondary" class="me-4" @click="dialog = false"> 关闭 </VBtn>
<VBtn @click="saveDashboardConfig"> <VBtn @click="saveDashboardConfig">
<template #prepend> <template #prepend>
<VIcon icon="mdi-content-save" /> <VIcon icon="mdi-content-save" />
@@ -397,78 +397,73 @@ onDeactivated(() => {
</VDialog> </VDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.settings-card {
overflow: hidden;
border-radius: 12px;
}
.settings-card-header { .settings-card-header {
background-color: rgba(var(--v-theme-primary), 0.03); padding-block: 16px;
padding-inline: 20px;
} }
.settings-hint { .settings-hint {
color: rgba(var(--v-theme-on-surface), 0.6); color: rgba(var(--v-theme-on-surface), 0.7);
font-size: 0.9rem; font-size: 0.9rem;
margin-block-end: 16px; margin-block-end: 16px;
} }
.settings-grid { .settings-grid {
display: grid; display: grid;
gap: 8px; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.setting-label {
color: rgba(var(--v-theme-on-surface), 0.8);
font-size: 0.9rem;
transition: color 0.2s ease;
}
.setting-item {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&::before {
position: absolute;
background-color: transparent;
block-size: 100%;
content: '';
inline-size: 4px;
inset-block-start: 0;
inset-inline-start: 0;
transition: background-color 0.3s ease;
}
&:hover {
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-surface-variant), 0.6);
}
&.enabled {
border-color: rgba(var(--v-theme-primary), 0.5);
background-color: rgba(var(--v-theme-primary), 0.05);
.setting-label {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
}
} }
.setting-item-inner { .setting-item-inner {
display: flex; display: flex;
align-items: center; align-items: center;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface), 1);
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
}
}
.setting-item {
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
&.enabled {
.setting-item-inner {
border-color: rgba(var(--v-theme-primary), 0.2);
background-color: rgba(var(--v-theme-primary), 0.08);
}
}
&.电影 .setting-item-inner {
border-inline-start: 3px solid #3b82f6;
}
&.电视剧 .setting-item-inner {
border-inline-start: 3px solid #6366f1;
}
&.动漫 .setting-item-inner {
border-inline-start: 3px solid #a855f7;
}
&.榜单 .setting-item-inner {
border-inline-start: 3px solid #f59e0b;
}
} }
.setting-check { .setting-check {
margin-inline-end: 8px; margin-inline-end: 8px;
} }
.setting-label {
overflow: hidden;
font-size: 0.9rem;
text-overflow: ellipsis;
white-space: nowrap;
}
</style> </style>
+141 -15
View File
@@ -7,6 +7,7 @@ import BangumiView from '@/views/discover/BangumiView.vue'
import ExtraSourceView from '@/views/discover/ExtraSourceView.vue' import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types' import { DiscoverSource } from '@/api/types'
import api from '@/api' import api from '@/api'
import { or } from '@vueuse/math'
const activeTab = ref('') const activeTab = ref('')
@@ -19,9 +20,19 @@ const orderConfig = ref<{ name: string }[]>([])
// 标签页 // 标签页
const discoverTabs = ref<DiscoverSource[]>([]) const discoverTabs = ref<DiscoverSource[]>([])
// 标签页项
const discoverTabItems = computed(() => {
return discoverTabs.value.map(item => ({
title: item.name,
}))
})
// 额外的数据源 // 额外的数据源
const extraDiscoverSources = ref<DiscoverSource[]>([]) const extraDiscoverSources = ref<DiscoverSource[]>([])
// 排序对话框
const orderConfigDialog = ref(false)
// 初始化发现标签 // 初始化发现标签
function initDiscoverTabs() { function initDiscoverTabs() {
for (const tab of DiscoverTabs) { for (const tab of DiscoverTabs) {
@@ -85,6 +96,7 @@ async function loadOrderConfig() {
// 保存顺序设置 // 保存顺序设置
async function saveTabOrder() { async function saveTabOrder() {
orderConfigDialog.value = false
// 顺序配置 // 顺序配置
const orderObj = discoverTabs.value.map(item => ({ name: item.name })) const orderObj = discoverTabs.value.map(item => ({ name: item.name }))
orderConfig.value = orderObj orderConfig.value = orderObj
@@ -106,7 +118,7 @@ onBeforeMount(async () => {
sortSubscribeOrder() sortSubscribeOrder()
// 选中第一个标签页 // 选中第一个标签页
if (discoverTabs.value.length > 0) { if (discoverTabs.value.length > 0) {
activeTab.value = discoverTabs.value[0].mediaid_prefix activeTab.value = discoverTabs.value[0].name
} }
}) })
@@ -114,44 +126,46 @@ onActivated(async () => {
await loadExtraDiscoverSources() await loadExtraDiscoverSources()
sortSubscribeOrder() sortSubscribeOrder()
}) })
</script> </script>
<template> <template>
<div> <div>
<VTabs v-model="activeTab" show-arrows stacked> <VHeaderTab :items="discoverTabItems" v-model="activeTab">
<draggable v-model="discoverTabs" handle=".tab-move" item-key="tab" tag="div" @end="saveTabOrder"> <template #append>
<template #item="{ element }"> <VBtn
<VTab :key="element.mediaid_prefix" :value="element.mediaid_prefix" class="px-10 rounded-t-lg"> icon="mdi-order-alphabetical-ascending"
<span class="tab-move">{{ element.name }}</span> variant="text"
</VTab> color="primary"
</template> size="default"
</draggable> class="settings-icon-button"
</VTabs> @click="orderConfigDialog = true"
/>
</template>
</VHeaderTab>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false"> <VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="themoviedb"> <VWindowItem value="TheMovieDb">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<TheMovieDbView /> <TheMovieDbView />
</div> </div>
</transition> </transition>
</VWindowItem> </VWindowItem>
<VWindowItem value="douban"> <VWindowItem value="豆瓣">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<DoubanView /> <DoubanView />
</div> </div>
</transition> </transition>
</VWindowItem> </VWindowItem>
<VWindowItem value="bangumi"> <VWindowItem value="Bangumi">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<BangumiView /> <BangumiView />
</div> </div>
</transition> </transition>
</VWindowItem> </VWindowItem>
<VWindowItem v-for="item in extraDiscoverSources" :key="item.mediaid_prefix" :value="item.mediaid_prefix"> <VWindowItem v-for="item in extraDiscoverSources" :key="item.mediaid_prefix" :value="item.name">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<ExtraSourceView :source="item" /> <ExtraSourceView :source="item" />
@@ -159,7 +173,119 @@ onActivated(async () => {
</transition> </transition>
</VWindowItem> </VWindowItem>
</VWindow> </VWindow>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="orderConfigDialog" v-model="orderConfigDialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-order-alphabetical-ascending" size="small" class="me-2" />
设置标签顺序
</VCardTitle>
<DialogCloseBtn @click="orderConfigDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<p class="settings-hint">拖动对标签页进行排序</p>
<draggable
v-model="discoverTabs"
handle=".cursor-move"
item-key="mediaid_prefix"
tag="div"
:component-data="{ 'class': 'settings-grid' }"
>
<template #item="{ element }">
<div class="setting-item enabled">
<div class="setting-item-inner cursor-move text-center">
<span class="setting-label">{{ element.name }}</span>
</div>
</div>
</template>
</draggable>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VSpacer />
<VBtn @click="saveTabOrder">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
<!-- 快速滚动到顶部按钮 --> <!-- 快速滚动到顶部按钮 -->
<VScrollToTopBtn /> <VScrollToTopBtn />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.settings-card-header {
padding-block: 16px;
padding-inline: 20px;
}
.settings-hint {
color: rgba(var(--v-theme-on-surface), 0.7);
font-size: 0.9rem;
margin-block-end: 16px;
}
.settings-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.setting-label {
color: rgba(var(--v-theme-on-surface), 0.8);
font-size: 0.9rem;
transition: color 0.2s ease;
}
.setting-item {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&::before {
position: absolute;
background-color: transparent;
block-size: 100%;
content: '';
inline-size: 4px;
inset-block-start: 0;
inset-inline-start: 0;
transition: background-color 0.3s ease;
}
&:hover {
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-surface-variant), 0.6);
}
&.enabled {
border-color: rgba(var(--v-theme-primary), 0.5);
background-color: rgba(var(--v-theme-primary), 0.05);
.setting-label {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
}
}
.setting-item-inner {
display: flex;
align-items: center;
}
.setting-check {
margin-inline-end: 8px;
}
</style>
+8 -11
View File
@@ -2,7 +2,6 @@
import api from '@/api' import api from '@/api'
import { DownloaderConf } from '@/api/types' import { DownloaderConf } from '@/api/types'
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue' import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
import router from '@/router'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
const route = useRoute() const route = useRoute()
@@ -11,6 +10,13 @@ const activeTab = ref(route.query.tab)
// 下载器 // 下载器
const downloaders = ref<DownloaderConf[]>([]) const downloaders = ref<DownloaderConf[]>([])
// 下载器字典
const downloaderItems = computed(() => {
return downloaders.value.map(item => ({
title: item.name,
}))
})
// 调用API查询下载器设置 // 调用API查询下载器设置
async function loadDownloaderSetting() { async function loadDownloaderSetting() {
try { try {
@@ -22,10 +28,6 @@ async function loadDownloaderSetting() {
} }
} }
function jumpTab(tab: string) {
router.push('/subscribe/movie?tab=' + tab)
}
onMounted(async () => { onMounted(async () => {
await loadDownloaderSetting() await loadDownloaderSetting()
}) })
@@ -37,12 +39,7 @@ onActivated(async () => {
<template> <template>
<div v-if="downloaders.length > 0"> <div v-if="downloaders.length > 0">
<VTabs v-model="activeTab" show-arrows stacked> <VHeaderTab :items="downloaderItems" v-model="activeTab" />
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)" class="px-10 rounded-t-lg">
{{ item.name }}
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false"> <VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem v-for="item in downloaders" :value="item.name"> <VWindowItem v-for="item in downloaders" :value="item.name">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
+3 -9
View File
@@ -236,16 +236,14 @@ watch(currentCategory, () => {
</div> </div>
<!-- 设置面板 --> <!-- 设置面板 -->
<VDialog v-model="dialog" width="500" class="settings-dialog" scrollable> <VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable>
<VCard class="settings-card"> <VCard class="settings-card">
<VCardItem class="settings-card-header"> <VCardItem class="settings-card-header">
<VCardTitle> <VCardTitle>
<VIcon icon="mdi-tune" size="small" class="me-2" /> <VIcon icon="mdi-tune" size="small" class="me-2" />
自定义内容 自定义内容
</VCardTitle> </VCardTitle>
<template #append> <DialogCloseBtn @click="dialog = false" />
<VBtn icon="mdi-close" variant="text" @click="dialog = false" />
</template>
</VCardItem> </VCardItem>
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -347,10 +345,6 @@ watch(currentCategory, () => {
} }
/* Settings Dialog Styles */ /* Settings Dialog Styles */
.settings-dialog .v-card {
border-radius: 12px;
}
.settings-card-header { .settings-card-header {
padding-block: 16px; padding-block: 16px;
padding-inline: 20px; padding-inline: 20px;
@@ -365,7 +359,7 @@ watch(currentCategory, () => {
.settings-grid { .settings-grid {
display: grid; display: grid;
gap: 12px; gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
} }
.setting-label { .setting-label {
+1 -1
View File
@@ -21,7 +21,7 @@ const subscribeEditDialog = ref(false)
<VHeaderTab :items="subType == '电影' ? SubscribeMovieTabs : SubscribeTvTabs" v-model="activeTab"> <VHeaderTab :items="subType == '电影' ? SubscribeMovieTabs : SubscribeTvTabs" v-model="activeTab">
<template #append> <template #append>
<VBtn <VBtn
icon="mdi-clipboard-edit" icon="mdi-clipboard-edit-outline"
variant="text" variant="text"
color="primary" color="primary"
size="default" size="default"