Compare commits

..

6 Commits

Author SHA1 Message Date
jxxghp
18d778a1cc feat:聚合搜索支持订阅 2024-06-02 19:50:28 +08:00
jxxghp
d667c4e45d v1.9.3 2024-06-02 18:50:55 +08:00
jxxghp
b7f8ffd56f fix 聚合搜索 2024-06-02 18:45:50 +08:00
jxxghp
c20f9d527f fix icon 2024-06-02 15:41:09 +08:00
jxxghp
b859d00cb9 fix 聚合搜索 2024-06-02 14:58:58 +08:00
jxxghp
a2d28ad360 feat:聚合搜索(working...) 2024-06-02 11:13:03 +08:00
17 changed files with 733 additions and 365 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.9.2-4",
"version": "1.9.3-1",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -19,7 +19,6 @@
]
},
"dependencies": {
"@bytebase/vue-kbar": "^0.1.8",
"@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8",
"@fullcalendar/interaction": "^6.1.7",
@@ -37,6 +36,7 @@
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"nprogress": "^0.2.0",
"qrcode.vue": "^3.4.1",
"sass": "^1.59.3",

View File

@@ -14,7 +14,10 @@ function onClick() {
</script>
<template>
<IconBtn :class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'" @click.stop="onClick">
<IconBtn
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'"
@click.stop="onClick"
>
<VIcon icon="mdi-close" />
</IconBtn>
</template>

View File

@@ -120,6 +120,12 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
disable?: boolean
}
export interface NavMenu extends NavLink {
header: string
admin: boolean
description?: string
}
// 👉 Vertical nav group
export interface NavGroup extends Partial<AclProperties> {
title: string

View File

@@ -63,6 +63,8 @@ export interface Subscribe {
date: string
// 编辑框设置项
show_edit_dialog: boolean
// 编辑框打开状态
page_open?: boolean
}
// 历史记录
@@ -445,6 +447,8 @@ export interface Plugin {
history?: { [key: string]: string }
// 添加时间
add_time?: number
// 页面打开状态
page_open?: boolean
}
// 渲染结构

View File

@@ -365,11 +365,19 @@ const dropdownItems = ref([
// 监听插件状态变化
watch(
() => props.plugin?.has_update,
(newHasUpdate, oldHasUpdate) => {
(newHasUpdate, _) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1) dropdownItems.value[updateItemIndex].show = newHasUpdate
},
)
// 监听插件窗口状态变化
watch(
() => props.plugin?.page_open,
(newOpenState, _) => {
if (newOpenState) openPluginDetail()
},
)
</script>
<template>

View File

@@ -133,6 +133,14 @@ const dropdownItems = ref([
},
},
])
// 监听插件窗口状态变化
watch(
() => props.media?.page_open,
(newOpenState, _) => {
if (newOpenState) editSubscribeDialog()
},
)
</script>
<template>

View File

@@ -2,8 +2,6 @@
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserNofification from '@/layouts/components/UserNotification.vue'
@@ -11,9 +9,16 @@ import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
import { SystemNavMenus } from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
}
</script>
<template>
@@ -25,113 +30,44 @@ const superUser = store.state.auth.superUser
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Search Bar -->
<SearchBar />
<!-- 👉 Spacer -->
<VSpacer />
<!-- 👉 Shortcuts -->
<ShortcutBar v-if="superUser" />
<!-- 👉 Theme -->
<NavbarThemeSwitcher />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->
<UserProfile />
</div>
</template>
<template #vertical-nav-content>
<VerticalNavLink
:item="{
title: '仪表板',
icon: 'mdi-home-outline',
to: '/dashboard',
}"
/>
<VerticalNavLink v-for="item in getMenuList('开始')" :item="item" />
<!-- 👉 发现 -->
<VerticalNavSectionTitle
:item="{
heading: '发现',
}"
/>
<VerticalNavLink
:item="{
title: '推荐',
icon: 'mdi-table-star',
to: '/ranking',
}"
/>
<VerticalNavLink
:item="{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
}"
/>
<VerticalNavLink v-for="item in getMenuList('发现')" :item="item" />
<!-- 👉 订阅 -->
<VerticalNavSectionTitle
:item="{
heading: '订阅',
}"
/>
<VerticalNavLink
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
to: '/subscribe-movie?tab=mysub',
}"
/>
<VerticalNavLink
:item="{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv?tab=mysub',
}"
/>
<VerticalNavLink
:item="{
title: '日历',
icon: 'mdi-calendar',
to: '/calendar',
}"
/>
<VerticalNavLink v-for="item in getMenuList('订阅')" :item="item" />
<!-- 👉 整理 -->
<VerticalNavSectionTitle
:item="{
heading: '整理',
}"
/>
<VerticalNavLink
:item="{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
}"
/>
<VerticalNavLink v-for="item in getMenuList('整理')" :item="item" />
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="superUser"
@@ -139,37 +75,12 @@ const superUser = store.state.auth.superUser
heading: '系统',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '插件',
icon: 'mdi-apps',
to: '/plugins?tab=installed',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '设定',
icon: 'mdi-cog',
to: '/setting?tab=account',
}"
/>
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
</template>
<template #after-vertical-nav-items />
<!-- 👉 Pages -->
<slot />
<!-- 👉 Footer -->
<template #footer>
<Footer />

View File

@@ -1,109 +1,39 @@
<script lang="ts" setup>
// 路由
const router = useRouter()
import * as Mousetrap from 'mousetrap'
import SearchBarView from '@/views/system/SearchBarView.vue'
// 搜索词
const searchWord = ref(null)
// 搜索弹窗
const searchDialog = ref(false)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// 当前的搜索类型 media/person
const searchType = ref('media')
// 搜索提示词列表
const searchHintList = ref<string[]>([])
// Search
function search() {
if (!searchWord.value) return
if (!searchHintList.value.includes(searchWord.value)) searchHintList.value.push(searchWord.value)
searchDialog.value = false
router.push({
path: '/browse/media/search',
query: {
title: searchWord.value,
type: searchType.value,
},
})
}
// 切换搜索类型
function switchSearchType() {
searchType.value = searchType.value === 'media' ? 'person' : 'media'
}
// 注册快捷键
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
// 打开搜索弹窗
function openSearchDialog() {
searchDialog.value = true
nextTick(() => {
searchWordInput.value?.focus()
})
return false
}
</script>
<template>
<!-- 👉 Search Button -->
<div class="d-flex align-center cursor-pointer" style="user-select: none">
<VDialog v-model="searchDialog" max-width="50rem" transition="dialog-top-transition">
<!-- Dialog Content -->
<VCard title="搜索">
<VCardText>
<VRow>
<VCol cols="12">
<VCombobox
ref="searchWordInput"
v-model="searchWord"
:items="searchHintList"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
@keydown.enter="search"
@click:prepend-inner="switchSearchType"
clearable
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" @click="search"> 搜索 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
<!-- 👉 Search Icon -->
<IconBtn class="d-md-none" @click="openSearchDialog">
<VIcon icon="mdi-magnify" />
</IconBtn>
<!-- 👉 Search Textfield -->
<span class="w-full me-3">
<VCombobox
key="search_navbar"
v-model="searchWord"
:items="searchHintList"
class="d-none d-md-block text-disabled search-box"
density="compact"
variant="solo"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
append-inner-icon="mdi-magnify"
single-line
hide-details
flat
rounded
@click:append-inner="search"
@click:prepend-inner="switchSearchType"
@keydown.enter="search"
/>
</span>
<div class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
<IconBtn @click="openSearchDialog">
<VIcon icon="ri-search-line" />
</IconBtn>
<span class="d-none d-md-flex align-center text-disabled ms-2" @click="openSearchDialog">
<span class="me-3">搜索</span>
<span class="meta-key">K</span>
</span>
</div>
<!-- 搜索弹窗 -->
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
</template>
<style lang="scss">
.search-box div.v-input__control div[role='textbox'] {
border: 1px solid rgb(var(--v-theme-background));
<style type="scss" scoped>
.meta-key {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
block-size: 1.75rem;
padding-block: 0.1rem;
padding-inline: 0.25rem;
}
</style>

View File

@@ -11,75 +11,27 @@ import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import { SettingTabs } from '@/router/menu'
const route = useRoute()
const activeTab = ref(route.query.tab)
// tabs
const tabs = [
{
title: '用户',
icon: 'mdi-account',
tab: 'account',
},
{
title: '连接',
icon: 'mdi-server-network',
tab: 'system',
},
{
title: '目录',
icon: 'mdi-folder',
tab: 'directory',
},
{
title: '站点',
icon: 'mdi-web',
tab: 'site',
},
{
title: '搜索',
icon: 'mdi-magnify',
tab: 'search',
},
{
title: '订阅',
icon: 'mdi-rss',
tab: 'subscribe',
},
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
},
{
title: '通知',
icon: 'mdi-bell',
tab: 'notification',
},
{
title: '词表',
icon: 'mdi-file-word-box',
tab: 'words',
},
{
title: '关于',
icon: 'mdi-information',
tab: 'about',
},
]
// 跳转tab
function jumpTab(tab: string) {
router.push("/setting?tab=" + tab)
router.push('/setting?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab" @click="jumpTab(item.tab)" selected-class="v-slide-group-item--active v-tab--selected">
<VTab
v-for="item in SettingTabs"
:key="item.icon"
:value="item.tab"
@click="jumpTab(item.tab)"
selected-class="v-slide-group-item--active v-tab--selected"
>
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</VTab>

View File

@@ -2,34 +2,26 @@
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import router from '@/router'
import { SubscribeMovieTabs } from '@/router/menu'
const route = useRoute()
// 标签页
const tabs = [
{
title: '我的订阅',
tab: 'mysub',
},
{
title: '热门订阅',
tab: 'popular',
},
]
// 订阅ID参数
const subId = ref(route.query.id as string)
// 当前标签
const activeTab = ref(route.query.tab)
// 跳转tab
function jumpTab(tab: string) {
router.push("/subscribe-movie?tab=" + tab)
router.push('/subscribe-movie?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab" @click="jumpTab(item.tab)">
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
@@ -37,12 +29,12 @@ function jumpTab(tab: string) {
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="mysub">
<transition name="fade-slide" appear>
<SubscribeListView type="电影" />
<SubscribeListView type="电影" :subid="subId" />
</transition>
</VWindowItem>
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<SubscribePopularView type="电影" />
<SubscribePopularView type="电影" :subid="subId" />
</transition>
</VWindowItem>
</VWindow>

View File

@@ -2,34 +2,25 @@
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import router from '@/router'
import { SubscribeTvTabs } from '@/router/menu'
const route = useRoute()
// 标签页
const tabs = [
{
title: '我的订阅',
tab: 'mysub',
},
{
title: '热门订阅',
tab: 'popular',
},
]
// 当前标签
const activeTab = ref(route.query.tab)
// 订阅ID参数
const subId = ref(route.query.id as string)
// 跳转tab
function jumpTab(tab: string) {
router.push("/subscribe-tv?tab=" + tab)
router.push('/subscribe-tv?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab" @click="jumpTab(item.tab)">
<VTab v-for="item in SubscribeTvTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
@@ -37,12 +28,12 @@ function jumpTab(tab: string) {
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="mysub">
<transition name="fade-slide" appear>
<SubscribeListView type="电视剧" />
<SubscribeListView type="电视剧" :subid="subId" />
</transition>
</VWindowItem>
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<SubscribePopularView type="电视剧" />
<SubscribePopularView type="电视剧" :subid="subId" />
</transition>
</VWindowItem>
</VWindow>

221
src/router/menu.ts Normal file
View File

@@ -0,0 +1,221 @@
// 导般菜单
export const SystemNavMenus = [
{
title: '仪表板',
icon: 'mdi-home-outline',
to: '/dashboard',
header: '开始',
admin: false,
},
{
title: '推荐',
icon: 'mdi-table-star',
to: '/ranking',
header: '发现',
admin: false,
},
{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
header: '发现',
admin: false,
},
{
title: '电影',
icon: 'mdi-movie-roll',
to: '/subscribe-movie?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '日历',
icon: 'mdi-calendar',
to: '/calendar',
header: '订阅',
admin: false,
},
{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
header: '整理',
admin: false,
},
{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
header: '整理',
admin: true,
},
{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
header: '整理',
admin: true,
},
{
title: '插件',
icon: 'mdi-apps',
to: '/plugins?tab=installed',
header: '系统',
admin: true,
},
{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
header: '系统',
admin: true,
},
{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
header: '系统',
admin: true,
},
]
// 常用菜单功能
export const UserfulMenus = [
{
title: '搜索设置',
icon: 'mdi-magnify',
to: 'setting?tab=search',
},
{
title: '订阅设置',
icon: 'mdi-rss',
to: 'setting?tab=subscribe',
},
{
title: '服务',
icon: 'mdi-list-box',
to: 'setting?tab=service',
},
{
title: '词表',
icon: 'mdi-file-word-box',
to: 'setting?tab=words',
},
{
title: '历史记录',
icon: 'mdi-history',
to: 'history',
},
]
// 设定标签页
export const SettingTabs = [
{
title: '用户',
icon: 'mdi-account',
tab: 'account',
description: '个人信息、用户管理、修改密码、双重认证',
},
{
title: '连接',
icon: 'mdi-server-network',
tab: 'system',
description: '下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex',
},
{
title: '目录',
icon: 'mdi-folder',
tab: 'directory',
description: '下载目录、媒体库目录、整理模式',
},
{
title: '站点',
icon: 'mdi-web',
tab: 'site',
description: '站点同步、下载优先规则、站点重置',
},
{
title: '搜索',
icon: 'mdi-magnify',
tab: 'search',
description: '媒体数据源TheMovieDb、豆瓣、Bangumi、搜索站点、搜索优先级、默认过滤规则',
},
{
title: '订阅',
icon: 'mdi-rss',
tab: 'subscribe',
description: '订阅站点、订阅模式、订阅优先级、洗版优先级、默认过滤规则',
},
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
description: '定时作业',
},
{
title: '通知',
icon: 'mdi-bell',
tab: 'notification',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、消息类型',
},
{
title: '词表',
icon: 'mdi-file-word-box',
tab: 'words',
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
},
{
title: '关于',
icon: 'mdi-information',
tab: 'about',
},
]
// 电影订阅标签页
export const SubscribeMovieTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-movie-roll',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-movie-roll',
},
]
// 电视剧订阅标签页
export const SubscribeTvTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-television-classic',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-television-classic',
},
]
// 插件标签页
export const PluginTabs = [
{
title: '我的插件',
tab: 'installed',
icon: 'mdi-puzzle',
},
{
title: '插件市场',
tab: 'market',
icon: 'mdi-store',
},
]

View File

@@ -10,6 +10,7 @@ import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDefer } from '@/@core/utils/dom'
import router from '@/router'
import { PluginTabs } from '@/router/menu'
const route = useRoute()
@@ -22,17 +23,8 @@ let deferApp = (_: number) => true
// 当前标签
const activeTab = ref(route.query.tab)
// 标签页
const tabs = [
{
title: '我的插件',
tab: 'installed',
},
{
title: '插件市场',
tab: 'market',
},
]
// 插件ID参数
const pluginId = ref(route.query.id)
// 当前排序字段
const activeSort = ref(null)
@@ -330,13 +322,20 @@ function jumpTab(tab: string) {
onBeforeMount(async () => {
await refreshData()
getPluginStatistics()
if (activeTab.value != 'market' && pluginId.value) {
// 找到这个插件
const plugin = dataList.value.find(item => item.id === pluginId.value)
if (plugin) {
plugin.page_open = true
}
}
})
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab" @click="jumpTab(item.tab)">
<VTab v-for="item in PluginTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>

View File

@@ -21,8 +21,7 @@ const transferExcludeWords = ref('')
async function queryCustomIdentifiers() {
try {
const result: { [key: string]: any } = await api.get('system/setting/CustomIdentifiers')
customIdentifiers.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customIdentifiers.value = result.data.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -32,8 +31,7 @@ async function queryCustomIdentifiers() {
async function queryCustomReleaseGroups() {
try {
const result: { [key: string]: any } = await api.get('system/setting/CustomReleaseGroups')
customReleaseGroups.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customReleaseGroups.value = result.data.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -43,8 +41,7 @@ async function queryCustomReleaseGroups() {
async function queryCustomization() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Customization')
customization.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customization.value = result.data?.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -54,8 +51,7 @@ async function queryCustomization() {
async function queryTransferExcludeWords() {
try {
const result: { [key: string]: any } = await api.get('system/setting/TransferExcludeWords')
transferExcludeWords.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) transferExcludeWords.value = result.data?.value.join('\n')
} catch (error) {
console.log(error)
}

View File

@@ -11,6 +11,7 @@ import store from '@/store'
// 输入参数
const props = defineProps({
type: String,
subid: String,
})
// 是否刷新过
@@ -35,9 +36,6 @@ async function fetchData() {
}
}
// 加载时获取数据
onBeforeMount(fetchData)
// 刷新状态
const loading = ref(false)
@@ -56,6 +54,18 @@ const filteredDataList = computed(() => {
if (superUser) return dataList.value.filter(data => data.type === props.type)
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
})
onMounted(async () => {
await fetchData()
if (props.subid) {
// 找到这个订阅
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
if (sub) {
// 打开编辑弹窗
sub.page_open = true
}
}
})
</script>
<template>

View File

@@ -0,0 +1,386 @@
<script setup lang="ts">
import api from '@/api'
import type { Plugin, Subscribe } from '@/api/types'
import {
SystemNavMenus,
UserfulMenus,
SubscribeMovieTabs,
SubscribeTvTabs,
PluginTabs,
SettingTabs,
} from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
// 路由
const router = useRouter()
// 定义事件
const emit = defineEmits(['close'])
// 搜索词
const searchWord = ref(null)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// 搜索提示词列表
const searchHintList = ref<string[]>([])
// 所有菜单功能
function getMenus(): NavMenu[] {
let menus: NavMenu[] = []
// 导航菜单
for (const key in SystemNavMenus) {
menus.push({
title: SystemNavMenus[key].title,
icon: SystemNavMenus[key].icon,
to: SystemNavMenus[key].to,
header: SystemNavMenus[key].header,
admin: SystemNavMenus[key].admin,
})
}
// 各类标签页
for (const key in SettingTabs) {
menus.push({
title: '设定 -> ' + SettingTabs[key].title,
icon: SettingTabs[key].icon,
to: `/setting?tab=${SettingTabs[key].tab}`,
header: '',
admin: true,
description: SettingTabs[key].description,
})
}
for (const key in SubscribeMovieTabs) {
menus.push({
title: '电影 -> ' + SubscribeMovieTabs[key].title,
icon: SubscribeMovieTabs[key].icon,
to: `/subscribe-movie?tab=${SubscribeMovieTabs[key].tab}`,
header: '',
admin: false,
})
}
for (const key in SubscribeTvTabs) {
menus.push({
title: '电视剧 -> ' + SubscribeTvTabs[key].title,
icon: SubscribeTvTabs[key].icon,
to: `/subscribe-tv?tab=${SubscribeTvTabs[key].tab}`,
header: '',
admin: false,
})
}
for (const key in PluginTabs) {
menus.push({
title: '插件 -> ' + PluginTabs[key].title,
icon: PluginTabs[key].icon,
to: `/plugins?tab=${PluginTabs[key].tab}`,
header: '',
admin: true,
})
}
return menus
}
// 匹配的菜单列表
const matchedMenuItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
const menuItems = getMenus()
if (menuItems)
return menuItems.filter(
item =>
item.title.toLowerCase().includes(lowerWord) ||
(item.description && item.description.toLowerCase().includes(lowerWord)),
)
return []
})
// 所有插件(已安装)
const pluginItems = ref<Plugin[]>([])
// 获取插件列表数据
async function fetchInstalledPlugins() {
try {
pluginItems.value = await api.get('plugin/', {
params: {
state: 'installed',
},
})
} catch (error) {
console.error(error)
}
}
// 区配的插件列表
const matchedPluginItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
return pluginItems.value.filter((item: Plugin) => {
if (!item.plugin_name && !item.plugin_desc) return false
return item.plugin_name?.toLowerCase().includes(lowerWord) || item.plugin_desc?.toLowerCase().includes(lowerWord)
})
})
// 所有订阅数据
const SubscribeItems = ref<Subscribe[]>([])
// 获取电影订阅列表数据
async function fetchSubscribes() {
try {
SubscribeItems.value = await api.get('subscribe/')
} catch (error) {
console.error(error)
}
}
// 匹配的订阅列表
const matchedSubscribeItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
return SubscribeItems.value.filter((item: Subscribe) => {
return item.name.toLowerCase().includes(lowerWord)
})
})
// 跳转媒体搜索页面
function searchMedia(searchType: string) {
// 搜索类型 media/person
if (!searchWord.value) return
if (!searchHintList.value.includes(searchWord.value)) searchHintList.value.push(searchWord.value)
router.push({
path: '/browse/media/search',
query: {
title: searchWord.value,
type: searchType,
},
})
emit('close')
}
// 跳转插件页面
function showPlugin(pluginId: string) {
router.push({
path: `/plugins/`,
query: {
tab: 'installed',
id: pluginId,
},
})
emit('close')
}
// 跳转菜单页面
function goPage(to: string) {
router.push(to)
emit('close')
}
// 跳转订阅页面
function goSubscribe(subscribe: Subscribe) {
if (subscribe.type === '电影') {
router.push({
path: '/subscribe-movie',
query: {
id: subscribe.id,
},
})
} else {
router.push({
path: '/subscribe-tv',
query: {
id: subscribe.id,
},
})
}
emit('close')
}
onMounted(() => {
setTimeout(() => {
searchWordInput.value?.focus()
}, 500)
fetchInstalledPlugins()
fetchSubscribes()
})
</script>
<template>
<VDialog max-width="40rem" scrollable>
<VCard>
<VCardText class="pe-12">
<VCombobox
ref="searchWordInput"
v-model="searchWord"
density="compact"
variant="plain"
class="text-high-emphasis"
placeholder="搜索 ..."
:items="searchHintList"
@keydown.enter="searchMedia('media')"
>
<template #prepend>
<VIcon icon="ri-search-line" style="opacity: 1" />
</template>
</VCombobox>
</VCardText>
<DialogCloseBtn inner-class="absolute right-3 top-5 text-high-emphasis" @click="emit('close')" />
<VDivider />
<div class="ps h-100">
<VList lines="one" v-if="searchWord">
<!-- 搜索结果 -->
<VListSubheader v-if="searchWord"> 媒体 </VListSubheader>
<VHover>
<template #default="hover">
<VListItem
prepend-icon="mdi-movie-search"
density="compact"
link
v-bind="hover.props"
@click="searchMedia('media')"
>
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }} </span> 相关的电影电视剧 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VHover>
<template #default="hover">
<VListItem
prepend-icon="mdi-account-search"
density="compact"
link
v-bind="hover.props"
@click="searchMedia('person')"
>
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的人物 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedSubscribeItems.length > 0"> 订阅 </VListSubheader>
<VHover
v-if="matchedSubscribeItems.length > 0"
v-for="subscribe in matchedSubscribeItems"
:key="subscribe.id"
>
<template #default="hover">
<VListItem
:prepend-icon="`${subscribe.type === '电影' ? 'mdi-movie-roll' : 'mdi-television-classic'}`"
density="compact"
link
v-bind="hover.props"
@click="goSubscribe(subscribe)"
>
<VListItemTitle>
{{ subscribe.name }}<span v-if="subscribe.season"> {{ subscribe.season }} </span>
</VListItemTitle>
<VListItemSubtitle> {{ subscribe.type }}</VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedMenuItems.length > 0"> 功能 </VListSubheader>
<VHover v-if="matchedMenuItems.length > 0" v-for="menu in matchedMenuItems" :key="menu.title">
<template #default="hover">
<VListItem
:prepend-icon="menu.icon as string"
density="compact"
link
v-bind="hover.props"
@click="goPage(menu.to as string)"
>
<VListItemTitle>
{{ menu.title }}
</VListItemTitle>
<VListItemSubtitle v-if="menu.description"> {{ menu.description }} </VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedPluginItems.length > 0"> 插件 </VListSubheader>
<VHover v-if="matchedPluginItems.length > 0" v-for="plugin in matchedPluginItems" :key="plugin.id">
<template #default="hover">
<VListItem
prepend-icon="mdi-puzzle"
density="compact"
link
v-bind="hover.props"
@click="showPlugin(plugin.id ?? '')"
>
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
<VListItemSubtitle> {{ plugin.plugin_desc }} </VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
<div v-else>
<!-- 默认 -->
<VCardText>
<VRow>
<VCol cols="12" md="6">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
<VList lines="one">
<VHover v-for="(menu, index) in UserfulMenus" :key="index">
<template #default="hover">
<VListItem
:prepend-icon="menu.icon"
density="compact"
link
v-bind="hover.props"
@click="goPage(menu.to)"
>
<VListItemTitle>
{{ menu.title }}
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
</VCol>
<VCol cols="12" md="6">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用插件</p>
<VList lines="one">
<VHover v-for="plugin in pluginItems.slice(0, 5)" :key="plugin.id">
<template #default="hover">
<VListItem
prepend-icon="mdi-puzzle"
density="compact"
link
v-bind="hover.props"
@click="showPlugin(plugin.id ?? '')"
>
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
</VCol>
<VCol cols="12" md="6"> </VCol>
<VCol cols="12" md="6"> </VCol>
</VRow>
</VCardText>
</div>
</div>
</VCard>
</VDialog>
</template>

View File

@@ -1023,13 +1023,6 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.23.8":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e"
integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.24.0":
version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50"
@@ -1064,15 +1057,6 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@bytebase/vue-kbar@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@bytebase/vue-kbar/-/vue-kbar-0.1.8.tgz#79b0cab21e9c013cb8935aa5d94be76fa9e0be3e"
integrity sha512-XfF3JadEvWzFngpu1U6C3RvseBEoWXF51Y0D3gL2R7T8Lx+OHEoPCR8jxCMXzROCCJ+eheerKqEiFl5WXrWw4w==
dependencies:
"@vueuse/core" "^7.1.2"
match-sorter "^6.3.1"
tiny-invariant "^1.2.0"
"@csstools/css-parser-algorithms@^2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd"
@@ -2201,14 +2185,6 @@
"@vueuse/shared" "10.9.0"
vue-demi ">=0.14.7"
"@vueuse/core@^7.1.2":
version "7.7.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-7.7.1.tgz#fc284f4103de73c7fb79bc06579d8066790db511"
integrity sha512-PRRgbATMpoeUmkCEBtUeJgOwtew8s+4UsEd+Pm7MhkjL2ihCNrSqxNVtM6NFE4uP2sWnkGcZpCjPuNSxowJ1Ow==
dependencies:
"@vueuse/shared" "7.7.1"
vue-demi "*"
"@vueuse/math@^10.1.2":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/math/-/math-10.9.0.tgz#0db3cb27c893fa22c50351397c283d5b6df0f5bc"
@@ -2229,13 +2205,6 @@
dependencies:
vue-demi ">=0.14.7"
"@vueuse/shared@7.7.1":
version "7.7.1"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-7.7.1.tgz#77e312de7275380efce86b0079bd7938791a076b"
integrity sha512-rN2qd22AUl7VdBxihagWyhUNHCyVk9IpvBTTfHoLH9G7rGE552X1f+zeCfehuno0zXif13jPw+icW/wn2a0rnQ==
dependencies:
vue-demi "*"
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@@ -5104,14 +5073,6 @@ magic-string@^0.30.2, magic-string@^0.30.3, magic-string@^0.30.5, magic-string@^
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
match-sorter@^6.3.1:
version "6.3.4"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.4.tgz#afa779d8e922c81971fbcb4781c7003ace781be7"
integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==
dependencies:
"@babel/runtime" "^7.23.8"
remove-accents "0.5.0"
mathml-tag-names@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@@ -5294,6 +5255,11 @@ mlly@^1.2.0, mlly@^1.4.2, mlly@^1.5.0:
pkg-types "^1.0.3"
ufo "^1.3.2"
mousetrap@^1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -6008,11 +5974,6 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
remove-accents@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687"
integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==
request@^2.44.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@@ -6857,11 +6818,6 @@ through@^2.3.4:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
tiny-invariant@^1.2.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -7288,11 +7244,6 @@ vite@^5.2.8:
optionalDependencies:
fsevents "~2.3.3"
vue-demi@*:
version "0.14.8"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.8.tgz#00335e9317b45e4a68d3528aaf58e0cec3d5640a"
integrity sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==
vue-demi@>=0.14.7:
version "0.14.7"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"