feat: 添加推荐源类型字段,优化推荐页面分类逻辑和样式

This commit is contained in:
jxxghp
2025-04-08 19:14:16 +08:00
parent 89e4a68a03
commit 93686bd354
5 changed files with 157 additions and 90 deletions
+2
View File
@@ -1265,6 +1265,8 @@ export interface RecommendSource {
name: string name: string
// 媒体数据源API地址 // 媒体数据源API地址
api_path: string api_path: string
// 类型
type: string
} }
// 站点资源分类 // 站点资源分类
+11 -11
View File
@@ -15,30 +15,30 @@ const props: any = inject('rankingPropsKey')
<style lang="scss" scoped> <style lang="scss" scoped>
.title-wrapper { .title-wrapper {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
width: 100%; justify-content: space-between;
inline-size: 100%;
} }
.title-section { .title-section {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative;
} }
.title-badge { .title-badge {
width: 3px;
height: 16px;
background-color: rgb(var(--v-theme-primary));
margin-right: 8px;
border-radius: 2px; border-radius: 2px;
background-color: rgb(var(--v-theme-primary));
block-size: 16px;
inline-size: 3px;
margin-inline-end: 8px;
} }
.title-text { .title-text {
font-size: 1.1rem;
font-weight: 600;
color: rgba(var(--v-theme-on-background), 0.95);
margin: 0;
padding: 0; padding: 0;
margin: 0;
color: rgba(var(--v-theme-on-background), 0.95);
font-size: 1.2rem;
font-weight: 600;
} }
</style> </style>
+108 -17
View File
@@ -343,29 +343,44 @@ onDeactivated(() => {
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable> <VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>设置仪表板</VCardTitle> <VCardTitle>
<VIcon icon="mdi-tune" size="small" class="me-2" />
设置仪表板
</VCardTitle>
</VCardItem> </VCardItem>
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VRow> <p class="settings-hint">选择您想在页面显示的内容</p>
<VCol <div class="settings-grid">
<div
v-for="item in dashboardConfigs" v-for="item in dashboardConfigs"
:key="buildPluginDashboardId(item.id, item.key)" :key="buildPluginDashboardId(item.id, item.key)"
cols="6" class="setting-item"
md="4" :class="{
sm="4" 'enabled': enableConfig[buildPluginDashboardId(item.id, item.key)],
}"
@click="
enableConfig[buildPluginDashboardId(item.id, item.key)] =
!enableConfig[buildPluginDashboardId(item.id, item.key)]
"
> >
<VCheckbox <div class="setting-item-inner">
v-model="enableConfig[buildPluginDashboardId(item.id, item.key)]" <div class="setting-check">
:label="item.attrs?.title ?? item.name" <VIcon
/> :icon="
</VCol> enableConfig[buildPluginDashboardId(item.id, item.key)] ? 'mdi-check-circle' : 'mdi-circle-outline'
</VRow> "
<VRow> :color="enableConfig[buildPluginDashboardId(item.id, item.key)] ? 'primary' : undefined"
<VCol cols="12" md="6"> size="small"
<VSwitch v-model="isElevated" label="自适应组件高度" /> />
</VCol> </div>
</VRow> <span class="setting-label">{{ item.attrs?.title ?? item.name }}</span>
</div>
</div>
</div>
<p class="mt-3">
<VSwitch v-model="isElevated" label="自适应组件高度" />
</p>
</VCardText> </VCardText>
<VDivider /> <VDivider />
<VCardText class="pt-5 text-end"> <VCardText class="pt-5 text-end">
@@ -381,3 +396,79 @@ onDeactivated(() => {
</VCard> </VCard>
</VDialog> </VDialog>
</template> </template>
<style lang="scss" scoped>
.settings-card {
overflow: hidden;
border-radius: 12px;
}
.settings-card-header {
background-color: rgba(var(--v-theme-primary), 0.03);
}
.settings-hint {
color: rgba(var(--v-theme-on-surface), 0.6);
font-size: 0.9rem;
margin-block-end: 16px;
}
.settings-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.setting-item-inner {
display: flex;
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 {
margin-inline-end: 8px;
}
.setting-label {
overflow: hidden;
font-size: 0.9rem;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+35 -62
View File
@@ -1,103 +1,93 @@
<script setup lang="ts"> <script setup lang="ts">
import api from '@/api' import api from '@/api'
import { useDisplay } from 'vuetify'
import { RecommendSource } from '@/api/types' import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue' import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
// APP
const display = useDisplay()
// 当前选择的分类 // 当前选择的分类
const currentCategory = ref('全部') const currentCategory = ref('全部')
// 定义分类类型 const viewList = reactive<{ apipath: string; linkurl: string; title: string; type: string }[]>([
type CategoryType = '全部' | '电影' | '电视剧' | '动漫' | '榜单'
type CategoryMap = Record<CategoryType, Array<{ apipath: string; linkurl: string; title: string }>>
// 预处理的分类视图数据
const categoryViewsMap = reactive<CategoryMap>({
全部: [],
电影: [],
电视剧: [],
动漫: [],
榜单: [],
})
// 按分类过滤视图的映射
const getCategoryForView = (title: string): CategoryType => {
if (title.includes('电影') || title.includes('热映') || (title.includes('TOP250') && !title.includes('剧集'))) {
return '电影'
} else if (title.includes('电视剧') || (title.includes('剧集') && !title.includes('动漫'))) {
return '电视剧'
} else if (title.includes('动漫') || title.includes('Bangumi')) {
return '动漫'
} else if (title.includes('TOP') || title.includes('榜') || title.includes('趋势')) {
return '榜单'
}
return '电影' // 默认分类
}
const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>([
{ {
apipath: 'recommend/tmdb_trending', apipath: 'recommend/tmdb_trending',
linkurl: '/browse/recommend/tmdb_trending?title=流行趋势', linkurl: '/browse/recommend/tmdb_trending?title=流行趋势',
title: '流行趋势', title: '流行趋势',
type: '榜单',
}, },
{ {
apipath: 'recommend/douban_showing', apipath: 'recommend/douban_showing',
linkurl: '/browse/recommend/douban_showing?title=正在热映', linkurl: '/browse/recommend/douban_showing?title=正在热映',
title: '正在热映', title: '正在热映',
type: '电影',
}, },
{ {
apipath: 'recommend/bangumi_calendar', apipath: 'recommend/bangumi_calendar',
linkurl: '/browse/recommend/bangumi_calendar?title=Bangumi每日放送', linkurl: '/browse/recommend/bangumi_calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送', title: 'Bangumi每日放送',
type: '动漫',
}, },
{ {
apipath: 'recommend/tmdb_movies', apipath: 'recommend/tmdb_movies',
linkurl: '/browse/recommend/tmdb_movies?title=TMDB热门电影', linkurl: '/browse/recommend/tmdb_movies?title=TMDB热门电影',
title: 'TMDB热门电影', title: 'TMDB热门电影',
type: '电影',
}, },
{ {
apipath: 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko', apipath: 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧', linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧',
title: 'TMDB热门电视剧', title: 'TMDB热门电视剧',
type: '电视剧',
}, },
{ {
apipath: 'recommend/douban_movie_hot', apipath: 'recommend/douban_movie_hot',
linkurl: '/browse/recommend/douban_movie_hot?title=豆瓣热门电影', linkurl: '/browse/recommend/douban_movie_hot?title=豆瓣热门电影',
title: '豆瓣热门电影', title: '豆瓣热门电影',
type: '电影',
}, },
{ {
apipath: 'recommend/douban_tv_hot', apipath: 'recommend/douban_tv_hot',
linkurl: '/browse/recommend/douban_tv_hot?title=豆瓣热门电视剧', linkurl: '/browse/recommend/douban_tv_hot?title=豆瓣热门电视剧',
title: '豆瓣热门电视剧', title: '豆瓣热门电视剧',
type: '电视剧',
}, },
{ {
apipath: 'recommend/douban_tv_animation', apipath: 'recommend/douban_tv_animation',
linkurl: '/browse/recommend/douban_tv_animation?title=豆瓣热门动漫', linkurl: '/browse/recommend/douban_tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫', title: '豆瓣热门动漫',
type: '动漫',
}, },
{ {
apipath: 'recommend/douban_movies', apipath: 'recommend/douban_movies',
linkurl: '/browse/recommend/douban_movies?title=豆瓣最新电影', linkurl: '/browse/recommend/douban_movies?title=豆瓣最新电影',
title: '豆瓣最新电影', title: '豆瓣最新电影',
type: '电影',
}, },
{ {
apipath: 'recommend/douban_tvs', apipath: 'recommend/douban_tvs',
linkurl: '/browse/recommend/douban_tvs?title=豆瓣最新电视剧', linkurl: '/browse/recommend/douban_tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧', title: '豆瓣最新电视剧',
type: '电视剧',
}, },
{ {
apipath: 'recommend/douban_movie_top250', apipath: 'recommend/douban_movie_top250',
linkurl: '/browse/recommend/douban_movie_top250?title=电影TOP250', linkurl: '/browse/recommend/douban_movie_top250?title=电影TOP250',
title: '豆瓣电影TOP250', title: '豆瓣电影TOP250',
type: '榜单',
}, },
{ {
apipath: 'recommend/douban_tv_weekly_chinese', apipath: 'recommend/douban_tv_weekly_chinese',
linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=豆瓣国产剧集榜', linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=豆瓣国产剧集榜',
title: '豆瓣国产剧集榜', title: '豆瓣国产剧集榜',
type: '榜单',
}, },
{ {
apipath: 'recommend/douban_tv_weekly_global', apipath: 'recommend/douban_tv_weekly_global',
linkurl: '/browse/recommend/douban_tv_weekly_global?title=豆瓣全球剧集榜', linkurl: '/browse/recommend/douban_tv_weekly_global?title=豆瓣全球剧集榜',
title: '豆瓣全球剧集榜', title: '豆瓣全球剧集榜',
type: '榜单',
}, },
]) ])
@@ -106,7 +96,7 @@ const filteredViews = computed(() => {
if (currentCategory.value === '全部') { if (currentCategory.value === '全部') {
return viewList.filter(item => enableConfig.value[item.title]) return viewList.filter(item => enableConfig.value[item.title])
} }
return categoryViewsMap[currentCategory.value as CategoryType] return viewList.filter(item => enableConfig.value[item.title] && item.type === currentCategory.value)
}) })
// 榜单启用配置, 以title为key // 榜单启用配置, 以title为key
@@ -120,21 +110,6 @@ const dialog = ref(false)
// 额外的数据源 // 额外的数据源
const extraRecommendSources = ref<RecommendSource[]>([]) const extraRecommendSources = ref<RecommendSource[]>([])
// 分类视图
function updateCategoryViews() {
// 清空所有分类
;(Object.keys(categoryViewsMap) as CategoryType[]).forEach(category => {
categoryViewsMap[category] = []
})
// 先把所有启用的视图按照分类归类
const enabledViews = viewList.filter(item => enableConfig.value[item.title])
enabledViews.forEach(view => {
const category = getCategoryForView(view.title)
categoryViewsMap[category].push(view)
})
}
// 加载额外的发现数据源 // 加载额外的发现数据源
async function loadExtraRecommendSources() { async function loadExtraRecommendSources() {
try { try {
@@ -145,10 +120,9 @@ async function loadExtraRecommendSources() {
apipath: source.api_path, apipath: source.api_path,
linkurl: `/browse/recommend/${source.api_path}?title=${source.name}`, linkurl: `/browse/recommend/${source.api_path}?title=${source.name}`,
title: source.name, title: source.name,
type: source.type,
})), })),
) )
// 添加新视图后更新分类
updateCategoryViews()
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -168,8 +142,6 @@ async function loadConfig() {
localStorage.setItem('MP_RECOMMEND', JSON.stringify(response.data.value)) localStorage.setItem('MP_RECOMMEND', JSON.stringify(response.data.value))
} }
} }
// 配置加载后更新分类
updateCategoryViews()
} }
// 设置项目 // 设置项目
@@ -185,13 +157,10 @@ async function saveConfig() {
console.error(error) console.error(error)
} }
dialog.value = false dialog.value = false
// 保存后更新分类
updateCategoryViews()
} }
// 标签图标映射 // 标签图标映射
const categoryIcons: Record<CategoryType, string> = { const categoryIcons: Record<string, string> = {
全部: 'mdi-filmstrip-box-multiple', 全部: 'mdi-filmstrip-box-multiple',
电影: 'mdi-movie', 电影: 'mdi-movie',
电视剧: 'mdi-television-classic', 电视剧: 'mdi-television-classic',
@@ -224,20 +193,19 @@ watch(currentCategory, () => {
<template> <template>
<div class="mp-recommend"> <div class="mp-recommend">
<!-- 页面顶部控制栏 --> <!-- 页面顶部控制栏 -->
<div class="recommend-header"> <div v-if="display.mdAndUp.value" class="recommend-header">
<div class="header-tabs"> <div class="header-tabs">
<div <div
v-for="(category, idx) in ['全部', '电影', '电视剧', '动漫', '榜单']" v-for="(icon, category) in categoryIcons"
:key="idx" :key="category"
class="header-tab" class="header-tab"
:class="{ 'active': currentCategory === category }" :class="{ 'active': currentCategory === category }"
@click="currentCategory = category" @click="currentCategory = category"
> >
<VIcon :icon="categoryIcons[category as CategoryType]" size="small" class="header-tab-icon" /> <VIcon :icon="icon" size="small" class="header-tab-icon" />
<span>{{ category }}</span> <span>{{ category }}</span>
</div> </div>
</div> </div>
<button class="tune-button" @click="dialog = true"> <button class="tune-button" @click="dialog = true">
<div class="tune-icon"> <div class="tune-icon">
<span></span> <span></span>
@@ -283,7 +251,7 @@ watch(currentCategory, () => {
class="setting-item" class="setting-item"
:class="{ :class="{
'enabled': enableConfig[item.title], 'enabled': enableConfig[item.title],
[getCategoryForView(item.title)]: true, [item.type]: true,
}" }"
@click="enableConfig[item.title] = !enableConfig[item.title]" @click="enableConfig[item.title] = !enableConfig[item.title]"
> >
@@ -301,7 +269,7 @@ watch(currentCategory, () => {
</div> </div>
</VCardText> </VCardText>
<VDivider /> <VDivider />
<VCardActions class="mt-3"> <VCardActions class="pt-5">
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))"> <VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
全选 全选
</VBtn> </VBtn>
@@ -309,8 +277,12 @@ watch(currentCategory, () => {
全不选 全不选
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn variant="text" @click="dialog = false">取消</VBtn> <VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
<VBtn color="primary" variant="tonal" class="px-3" @click="saveConfig"> 保存设置 </VBtn> <template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
@@ -333,6 +305,7 @@ watch(currentCategory, () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-radius: 8px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-primary), 0.02); background-color: rgba(var(--v-theme-primary), 0.02);
border-block-end: 1px solid rgba(var(--v-theme-primary), 0.1); border-block-end: 1px solid rgba(var(--v-theme-primary), 0.1);
@@ -575,7 +548,6 @@ watch(currentCategory, () => {
justify-content: space-between; justify-content: space-between;
block-size: 16px; block-size: 16px;
inline-size: 16px; inline-size: 16px;
margin-inline-end: 8px;
span { span {
display: block; display: block;
@@ -601,6 +573,7 @@ watch(currentCategory, () => {
.tune-text { .tune-text {
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
margin-inline-start: 8px;
} }
&:hover .tune-icon span { &:hover .tune-icon span {
+1
View File
@@ -281,6 +281,7 @@ html.v-overlay-scroll-blocked body {
} }
.v-overlay__content { .v-overlay__content {
padding-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
transition: opacity 0.2s ease !important; transition: opacity 0.2s ease !important;
} }