mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 01:50:10 +08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d667c4e45d | ||
|
|
b7f8ffd56f | ||
|
|
c20f9d527f | ||
|
|
b859d00cb9 | ||
|
|
a2d28ad360 | ||
|
|
c6702fbc18 | ||
|
|
5018f96786 | ||
|
|
f29f408b67 | ||
|
|
a475a3b851 | ||
|
|
9335f79c30 | ||
|
|
9dab691649 | ||
|
|
16abc65f49 | ||
|
|
23ac80886d | ||
|
|
b242e757e0 | ||
|
|
a69965a605 | ||
|
|
3321427eb4 | ||
|
|
3ffe354770 | ||
|
|
52e0d3a4bc | ||
|
|
e865a5ca62 | ||
|
|
528a4ddb03 | ||
|
|
36f3b649c6 | ||
|
|
ce91c0cc30 | ||
|
|
e31e9e3520 | ||
|
|
df313ebe7f | ||
|
|
e1cf36e952 | ||
|
|
493194652c | ||
|
|
5030e75c2c | ||
|
|
3c70eac7ca | ||
|
|
f9b22962a4 | ||
|
|
7ce0c21b0c | ||
|
|
7a7a8c923f | ||
|
|
d5d5e28f7e | ||
|
|
b22ac27075 | ||
|
|
3cb5f4bdfe | ||
|
|
d355e4575d | ||
|
|
bdbb118e55 | ||
|
|
9a174d99db | ||
|
|
9c8725066c | ||
|
|
9f0f3de864 | ||
|
|
ac84ed2d6a | ||
|
|
9d7e15f4df |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.3",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -36,8 +36,8 @@
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"pull-refresh-vue3": "^0.3.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"sass": "^1.59.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
@@ -50,7 +50,7 @@
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "3.5.14",
|
||||
"vuetify": "3.6.8",
|
||||
"vuetify-use-dialog": "^0.6.11",
|
||||
"vuex": "^4.1.0",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -212,7 +212,7 @@ onMounted(() => {
|
||||
</VList>
|
||||
</VMenu>
|
||||
<!-- 自定义 CSS -- -->
|
||||
<VDialog v-model="cssDialog" persistent max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard title="自定义主题风格">
|
||||
<DialogCloseBtn @click="cssDialog = false" />
|
||||
<VDivider />
|
||||
|
||||
6
src/@layouts/types.d.ts
vendored
6
src/@layouts/types.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -8,32 +8,33 @@ const api = axios.create({
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
// 在请求头中添加token
|
||||
const token = store.state.auth.token
|
||||
if (token)
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
// 添加响应拦截器
|
||||
api.interceptors.response.use((response) => {
|
||||
return response.data
|
||||
}, (error) => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(error)
|
||||
}
|
||||
else if (error.response.status === 403) {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(new Error(error))
|
||||
} else if (error.response.status === 403) {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
}
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
})
|
||||
return Promise.reject(new Error(error))
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
|
||||
@@ -445,6 +445,8 @@ export interface Plugin {
|
||||
history?: { [key: string]: string }
|
||||
// 添加时间
|
||||
add_time?: number
|
||||
// 页面打开状态
|
||||
page_open?: boolean
|
||||
}
|
||||
|
||||
// 渲染结构
|
||||
@@ -464,6 +466,8 @@ export interface DashboardItem {
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 插件的仪表板key
|
||||
key: string
|
||||
// 全局配置
|
||||
attrs: { [key: string]: any }
|
||||
// col列数
|
||||
@@ -716,12 +720,6 @@ export interface NotificationSwitch {
|
||||
vocechat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
export interface Setting {
|
||||
// 下载目录
|
||||
DOWNLOAD_PATH: string
|
||||
}
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
// 文件列表
|
||||
@@ -740,7 +738,7 @@ export interface EndPoints {
|
||||
|
||||
// 文件浏览项目
|
||||
export interface FileItem {
|
||||
// 类型
|
||||
// 类型 dir/file
|
||||
type: string
|
||||
// 文件名
|
||||
name: string
|
||||
|
||||
@@ -17,22 +17,30 @@ const props = defineProps({
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 路径
|
||||
const path = ref<string>('')
|
||||
|
||||
// 类型下拉字典
|
||||
const typeItems = [
|
||||
{ title: '全部', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
{ title: '动漫', value: '动漫' },
|
||||
]
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 路径更新
|
||||
function updatePath(value: string) {
|
||||
path.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// 根据选中的媒体类型,获取对应的媒体类别
|
||||
const getCategories = computed(() => {
|
||||
const default_value = [{ title: '全部', value: '' }]
|
||||
@@ -61,7 +69,11 @@ const getCategories = computed(() => {
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField v-model="props.directory.path" variant="underlined" label="路径" />
|
||||
<VPathField @update:modelValue="updatePath">
|
||||
<template #activator="{ menuprops }">
|
||||
<VTextField v-model="props.directory.path" v-bind="menuprops" variant="underlined" label="路径" />
|
||||
</template>
|
||||
</VPathField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
@@ -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>
|
||||
@@ -456,7 +464,7 @@ watch(
|
||||
<VCardText class="min-h-40">
|
||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<VFab icon="mdi-cog" location="bottom end" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ const transferForm = reactive({
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
scrape: true,
|
||||
scrape: false,
|
||||
})
|
||||
|
||||
// 所有媒体库目录
|
||||
@@ -73,14 +73,26 @@ const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 目的目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
return libraryDirectories.value.map(item => item.path)
|
||||
const directories = libraryDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 监听输入变化
|
||||
watchEffect(() => {
|
||||
transferForm.path = props.path ?? ''
|
||||
transferForm.target = props.target ?? null
|
||||
})
|
||||
|
||||
// 监听目的路径变化,自动查询目录的刮削配置
|
||||
watch(transferForm, async () => {
|
||||
if (transferForm.target) {
|
||||
const directory = libraryDirectories.value.find(item => item.path === transferForm.target)
|
||||
if (directory) {
|
||||
transferForm.scrape = directory.scrape ?? false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '请稍候 ...'
|
||||
|
||||
@@ -184,7 +184,9 @@ async function loadDownloadDirectories() {
|
||||
|
||||
// 保存目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
return downloadDirectories.value.map(item => item.path)
|
||||
// 去重后的下载目录
|
||||
const directories = downloadDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 质量选择框数据
|
||||
|
||||
89
src/components/input/PathField.vue
Normal file
89
src/components/input/PathField.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { FileItem } from '@/api/types'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
|
||||
// 输入变量为默认路径
|
||||
const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
default: '/',
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// update:modelValue 事件
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 激活的目录
|
||||
const activedDirs = ref<string[]>([])
|
||||
|
||||
// 打开的目录
|
||||
const openedDirs = ref<string[]>([])
|
||||
|
||||
// 目录列表
|
||||
const treeItems = ref<FileItem[]>([
|
||||
{
|
||||
name: '/',
|
||||
path: props.root,
|
||||
children: [],
|
||||
type: '',
|
||||
basename: props.root,
|
||||
extension: '',
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
},
|
||||
])
|
||||
|
||||
// 拉取子目录
|
||||
async function fetchDirs(item: any) {
|
||||
return api
|
||||
.get('/filebrowser/listdir?path=' + item.path)
|
||||
.then((data: any) => {
|
||||
item.children.push(...data)
|
||||
})
|
||||
.catch(err => console.warn(err))
|
||||
}
|
||||
|
||||
// 获取选择的目录路径
|
||||
const selectedPath = computed(() => {
|
||||
if (activedDirs.value.length > 0) {
|
||||
return activedDirs.value[0]
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 监听目录变化
|
||||
watch(activedDirs, newVal => {
|
||||
if (!newVal.length) return
|
||||
emit('update:modelValue', selectedPath)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDirs(treeItems.value[0])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VMenu :close-on-content-click="false" content-class="cursor-default">
|
||||
<template v-slot:activator="{ props }">
|
||||
<slot name="activator" :menuprops="props" />
|
||||
</template>
|
||||
<VTreeview
|
||||
v-model:activated="activedDirs"
|
||||
v-model:opened="openedDirs"
|
||||
:items="treeItems"
|
||||
:load-children="fetchDirs"
|
||||
item-key="path"
|
||||
item-title="name"
|
||||
item-value="path"
|
||||
item-type="unknown"
|
||||
activatable
|
||||
return-object
|
||||
max-height="20rem"
|
||||
expand-icon="mdi-folder"
|
||||
collapse-icon="mdi-folder-open"
|
||||
>
|
||||
</VTreeview>
|
||||
</VMenu>
|
||||
</template>
|
||||
@@ -42,7 +42,19 @@ onUnmounted(() => {
|
||||
<!-- 插件仪表板 -->
|
||||
<VHover v-else-if="!isNullOrEmptyObject(props.config)">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<!-- 无边框 -->
|
||||
<div v-if="props.config?.attrs.border === false">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardText class="p-0">
|
||||
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<div v-if="hover.isHovering" class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
<!-- 有边框 -->
|
||||
<VCard v-else v-bind="hover.props">
|
||||
<VCardItem v-if="props.config?.attrs.border !== false">
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
@@ -52,12 +64,9 @@ onUnmounted(() => {
|
||||
</VCardTitle>
|
||||
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText :class="{ 'p-0': props.config?.attrs.border === false }">
|
||||
<VCardText>
|
||||
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<div v-if="props.config?.attrs.border === false && hover.isHovering" class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
@@ -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',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
to: '/subscribe-tv',
|
||||
}"
|
||||
/>
|
||||
<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',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
to: '/site',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
to: '/setting',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
|
||||
</template>
|
||||
|
||||
<template #after-vertical-nav-items />
|
||||
|
||||
<!-- 👉 Pages -->
|
||||
<slot />
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<template #footer>
|
||||
<Footer />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -89,7 +89,7 @@ const avatar = store.state.auth.avatar
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Profile -->
|
||||
<VListItem v-if="superUser" link to="setting">
|
||||
<VListItem v-if="superUser" link @click="router.push('/setting?tab=account')">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import 'vue3-perfect-scrollbar/style.css'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import MediaCard from './components/cards/MediaCard.vue'
|
||||
import PosterCard from './components/cards/PosterCard.vue'
|
||||
@@ -25,6 +26,7 @@ import PersonCard from './components/cards/PersonCard.vue'
|
||||
import MediaInfoCard from './components/cards/MediaInfoCard.vue'
|
||||
import TorrentCard from './components/cards/TorrentCard.vue'
|
||||
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
|
||||
import PathField from './components/input/PathField.vue'
|
||||
import { fixArrayAt } from '@/@core/utils/compatibility'
|
||||
|
||||
// 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
@@ -48,6 +50,8 @@ app
|
||||
.component('VMediaInfoCard', MediaInfoCard)
|
||||
.component('VTorrentCard', TorrentCard)
|
||||
.component('VMediaIdSelector', MediaIdSelector)
|
||||
.component('VTreeview', VTreeview)
|
||||
.component('VPathField', PathField)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
|
||||
@@ -9,6 +9,20 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
|
||||
// 是否拉升高度
|
||||
const isElevated = ref(true)
|
||||
|
||||
// 计算属性,控制是否拉升高度
|
||||
const elevatedConf = controlledComputed(
|
||||
() => isElevated.value,
|
||||
() => ({
|
||||
class: { 'match-height': isElevated.value },
|
||||
}),
|
||||
)
|
||||
|
||||
// 所有组件刷新定时器的句柄
|
||||
const refreshTimers = ref<{ [key: string]: NodeJS.Timeout }>({})
|
||||
|
||||
// 仪表板启用配置
|
||||
const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
mediaStatistic: true,
|
||||
@@ -24,13 +38,14 @@ const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
})
|
||||
|
||||
// 仪表板顺序配置
|
||||
const orderConfig = ref<{ id: string }[]>([])
|
||||
const orderConfig = ref<{ id: string; key: string }[]>([])
|
||||
|
||||
// 仪表板配置
|
||||
const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'storage',
|
||||
name: '存储空间',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -38,6 +53,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'mediaStatistic',
|
||||
name: '媒体统计',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 8 },
|
||||
elements: [],
|
||||
@@ -45,6 +61,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'weeklyOverview',
|
||||
name: '最近入库',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -52,6 +69,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'speed',
|
||||
name: '实时速率',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -59,6 +77,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'scheduler',
|
||||
name: '后台任务',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -66,6 +85,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'cpu',
|
||||
name: 'CPU',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
@@ -73,6 +93,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'memory',
|
||||
name: '内存',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
@@ -80,6 +101,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'library',
|
||||
name: '我的媒体库',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
@@ -87,6 +109,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'playing',
|
||||
name: '继续观看',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
@@ -94,14 +117,15 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'latest',
|
||||
name: '最近添加',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
},
|
||||
])
|
||||
|
||||
// 有仪表板的插件
|
||||
const dashboardPlugins = ref<any[]>([])
|
||||
// 插件的仪表板元信息
|
||||
const pluginDashboardMeta = ref<any[]>([])
|
||||
|
||||
// 插件仪表板的刷新状态
|
||||
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||
@@ -133,6 +157,9 @@ async function loadDashboardConfig() {
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', JSON.stringify(orderConfig.value))
|
||||
}
|
||||
}
|
||||
// 是否拉升高度
|
||||
const local_elevated = localStorage.getItem('MP_DASHBOARD_ELEVATED')
|
||||
if (local_elevated) isElevated.value = local_elevated === 'true'
|
||||
// 排序
|
||||
if (orderConfig.value) {
|
||||
sortDashboardConfigs()
|
||||
@@ -142,28 +169,34 @@ async function loadDashboardConfig() {
|
||||
// 按order的顺序对dashboardConfigs进行排序
|
||||
function sortDashboardConfigs() {
|
||||
dashboardConfigs.value.sort((a, b) => {
|
||||
const aIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === a.id)
|
||||
const bIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === b.id)
|
||||
const aIndex = orderConfig.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === a.id && item.key === a.key,
|
||||
)
|
||||
const bIndex = orderConfig.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === b.id && item.key === b.key,
|
||||
)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function saveDashboardConfig() {
|
||||
async function saveDashboardConfig() {
|
||||
// 启用配置
|
||||
const data = JSON.stringify(enableConfig.value)
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
// 顺序配置,从dashboardConfigs中提取
|
||||
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id })))
|
||||
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id, key: item.key })))
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', order)
|
||||
// 是否拉升高度
|
||||
localStorage.setItem('MP_DASHBOARD_ELEVATED', isElevated.value.toString())
|
||||
// 保存到服务端
|
||||
try {
|
||||
api.post('/user/config/Dashboard', data, {
|
||||
await api.post('/user/config/Dashboard', data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
api.post('/user/config/DashboardOrder', order, {
|
||||
await api.post('/user/config/DashboardOrder', order, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -172,22 +205,29 @@ function saveDashboardConfig() {
|
||||
console.error(error)
|
||||
}
|
||||
// 保存后重新获取插件仪表板
|
||||
getDashboardPlugins()
|
||||
getPluginDashboardMeta()
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
// 调用API获取有仪表板的插件
|
||||
async function getDashboardPlugins() {
|
||||
// 只有超级用户才能获取插件仪表板
|
||||
// 构造插件仪表板主ID
|
||||
function buildPluginDashboardId(plugin_id: string, key: string) {
|
||||
if (!key) return plugin_id
|
||||
return plugin_id + ':' + key
|
||||
}
|
||||
|
||||
// 调用API获取所有插件的仪表板元信息
|
||||
async function getPluginDashboardMeta() {
|
||||
// 只有超级用户才能获取
|
||||
if (!superUser) return
|
||||
pluginDashboardMeta.value = await api.get('/plugin/dashboard/meta')
|
||||
try {
|
||||
dashboardPlugins.value = await api.get('/plugin/dashboards')
|
||||
if (!isNullOrEmptyObject(dashboardPlugins.value)) {
|
||||
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
||||
// 下载插件仪表板配置
|
||||
dashboardPlugins.value.forEach(async (plugin: { id: string }) => {
|
||||
pluginDashboardMeta.value.forEach(async (pluginDashboard: { id: string; key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
// 初始化插件仪表板的刷新状态
|
||||
pluginDashboardRefreshStatus.value[plugin.id] = true
|
||||
await getPluginDashboard(plugin.id)
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -196,12 +236,20 @@ async function getDashboardPlugins() {
|
||||
}
|
||||
|
||||
// 获取一个插件的仪表板配置项
|
||||
async function getPluginDashboard(id: string) {
|
||||
async function getPluginDashboard(id: string, key: string) {
|
||||
try {
|
||||
api.get(`/plugin/dashboard/${id}`).then((res: any) => {
|
||||
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
||||
api.get(url).then((res: any) => {
|
||||
if (res) {
|
||||
// 名称替换为元信息的名称
|
||||
const meta = pluginDashboardMeta.value.find(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (meta) res.name = meta.name
|
||||
// 保存到仪表板配置中,如果已经存在则替换
|
||||
const index = dashboardConfigs.value.findIndex((item: { id: string }) => item.id === id)
|
||||
const index = dashboardConfigs.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (index !== -1) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
@@ -209,11 +257,22 @@ async function getPluginDashboard(id: string) {
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
// 定时刷新
|
||||
if (res.attrs?.refresh && pluginDashboardRefreshStatus.value[id] && enableConfig.value[id]) {
|
||||
setTimeout(() => {
|
||||
getPluginDashboard(id)
|
||||
if (
|
||||
res.attrs?.refresh &&
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
|
||||
enableConfig.value[pluginDashboardId]
|
||||
) {
|
||||
// 清除之前的定时器
|
||||
if (refreshTimers.value[pluginDashboardId]) {
|
||||
clearTimeout(refreshTimers.value[pluginDashboardId])
|
||||
}
|
||||
// 设置新的定时器
|
||||
let timer = setTimeout(() => {
|
||||
getPluginDashboard(id, key)
|
||||
}, res.attrs.refresh * 1000)
|
||||
refreshTimers.value[pluginDashboardId] = timer
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -230,7 +289,7 @@ function dragOrderEnd() {
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadDashboardConfig()
|
||||
getDashboardPlugins()
|
||||
getPluginDashboardMeta()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -242,17 +301,20 @@ onBeforeMount(async () => {
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="VRow"
|
||||
:component-data="{ 'class': 'match-height' }"
|
||||
:component-data="elevatedConf"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCol v-if="enableConfig[element.id] && element.cols" v-bind:="element.cols">
|
||||
<DashboardElement :config="element" v-model:refreshStatus="pluginDashboardRefreshStatus[element.id]" />
|
||||
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
|
||||
<DashboardElement
|
||||
:config="element"
|
||||
v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
|
||||
<VFab icon="mdi-view-dashboard-edit" location="bottom" size="x-large" fixed app appear @click="dialog = true" />
|
||||
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog v-model="dialog" max-width="35rem" scrollable>
|
||||
@@ -263,8 +325,22 @@ onBeforeMount(async () => {
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="item in dashboardConfigs" :key="item.id" cols="6" md="4" sm="4">
|
||||
<VCheckbox v-model="enableConfig[item.id]" :label="item.attrs?.title ?? item.name" />
|
||||
<VCol
|
||||
v-for="item in dashboardConfigs"
|
||||
:key="buildPluginDashboardId(item.id, item.key)"
|
||||
cols="6"
|
||||
md="4"
|
||||
sm="4"
|
||||
>
|
||||
<VCheckbox
|
||||
v-model="enableConfig[buildPluginDashboardId(item.id, item.key)]"
|
||||
:label="item.attrs?.title ?? item.name"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="isElevated" label="自适应组件高度" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
@@ -133,21 +133,12 @@ onMounted(() => {
|
||||
<VFab
|
||||
v-if="viewType === 'list'"
|
||||
icon="mdi-view-grid"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('card')"
|
||||
/>
|
||||
<VFab
|
||||
v-else
|
||||
icon="mdi-view-list"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('list')"
|
||||
/>
|
||||
<VFab v-else icon="mdi-view-list" location="bottom" size="x-large" fixed app appear @click="setViewType('list')" />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
import router from '@/router'
|
||||
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
|
||||
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
@@ -10,70 +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.params.tab)
|
||||
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',
|
||||
},
|
||||
]
|
||||
function jumpTab(tab: string) {
|
||||
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">
|
||||
<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>
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/subscribe-movie?tab=' + tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
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)
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/subscribe-tv?tab=' + tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in SubscribeTvTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
@@ -10,8 +10,7 @@ const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
||||
if (to.meta.keepAlive && savedPosition)
|
||||
return savedPosition
|
||||
if (to.meta.keepAlive && savedPosition) return savedPosition
|
||||
return { top: 0 }
|
||||
},
|
||||
routes: [
|
||||
@@ -21,14 +20,14 @@ const router = createRouter({
|
||||
component: () => import('../layouts/default.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
path: '/dashboard',
|
||||
component: () => import('../pages/dashboard.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'ranking',
|
||||
path: '/ranking',
|
||||
component: () => import('../pages/ranking.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
@@ -36,63 +35,63 @@ const router = createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'resource',
|
||||
path: '/resource',
|
||||
component: () => import('../pages/resource.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-movie',
|
||||
path: '/subscribe-movie',
|
||||
component: () => import('../pages/subscribe-movie.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-tv',
|
||||
path: '/subscribe-tv',
|
||||
component: () => import('../pages/subscribe-tv.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
path: '/calendar',
|
||||
component: () => import('../pages/calendar.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'downloading',
|
||||
path: '/downloading',
|
||||
component: () => import('../pages/downloading.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
path: '/history',
|
||||
component: () => import('../pages/history.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'site',
|
||||
path: '/site',
|
||||
component: () => import('../pages/site.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
path: '/plugins',
|
||||
component: () => import('../pages/plugin.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
path: '/setting',
|
||||
component: () => import('../pages/setting.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
@@ -165,8 +164,7 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
startNProgress()
|
||||
next()
|
||||
}
|
||||
|
||||
221
src/router/menu.ts
Normal file
221
src/router/menu.ts
Normal 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',
|
||||
},
|
||||
]
|
||||
@@ -16,7 +16,7 @@ const variableTheme = controlledComputed(
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
|
||||
@@ -17,7 +17,7 @@ const variableTheme = controlledComputed(
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
|
||||
@@ -10,7 +10,7 @@ const headers = ['进程ID', '进程名称', '运行时间', '内存占用']
|
||||
const processList = ref<Process[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载数据
|
||||
async function loadProcessList() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ScheduleInfo } from '@/api/types'
|
||||
const schedulerList = ref<ScheduleInfo[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载定时服务列表
|
||||
async function loadSchedulerList() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import api from '@/api'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 下载器信息
|
||||
const downloadInfo = ref<DownloaderInfo>({
|
||||
|
||||
@@ -71,8 +71,8 @@ function initOptions(data: Context) {
|
||||
// 对季过滤选项进行排序
|
||||
const sortSeasonFilterOptions = computed(() => {
|
||||
return seasonFilterOptions.value.sort((a, b) => {
|
||||
// 按字符串升序排序
|
||||
return a.localeCompare(b, 'zh-Hans-CN', { sensitivity: 'accent' })
|
||||
// 按字符串降序排序
|
||||
return b.localeCompare(a)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,9 +105,9 @@ let defer = (_: number) => true
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
// 匹配过滤函数,filter中有任一值包含value则返回true
|
||||
const match = (filter: Array<string>, value: string | undefined): boolean =>
|
||||
filter.length === 0 || filter.includes(value ?? '') || filter.some(v => value?.includes(v) ?? false)
|
||||
|
||||
groupedDataList.value?.forEach(value => {
|
||||
if (value.length > 0) {
|
||||
@@ -231,7 +231,3 @@ watchEffect(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,8 @@ import noImage from '@images/logos/plugin.png'
|
||||
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()
|
||||
|
||||
@@ -19,19 +21,10 @@ const display = useDisplay()
|
||||
let deferApp = (_: number) => true
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的插件',
|
||||
tab: 'myplugin',
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
tab: 'pluginmarket',
|
||||
},
|
||||
]
|
||||
// 插件ID参数
|
||||
const pluginId = ref(route.query.id)
|
||||
|
||||
// 当前排序字段
|
||||
const activeSort = ref(null)
|
||||
@@ -320,24 +313,36 @@ function handleRepoUrl(url: string | undefined) {
|
||||
return url.replace('https://github.com/', '').replace('https://raw.githubusercontent.com/', '')
|
||||
}
|
||||
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/plugins?tab=' + tab)
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
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">
|
||||
<VTab v-for="item in PluginTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 我的插件 -->
|
||||
<VWindowItem value="myplugin">
|
||||
<VWindowItem value="installed">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
@@ -363,7 +368,7 @@ onBeforeMount(async () => {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- 插件市场 -->
|
||||
<VWindowItem value="pluginmarket">
|
||||
<VWindowItem value="market">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
@@ -437,7 +442,7 @@ onBeforeMount(async () => {
|
||||
<VFab
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
class="mb-2"
|
||||
size="x-large"
|
||||
fixed
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||
import api from '@/api'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
@@ -7,7 +7,7 @@ import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<DownloadingInfo[]>([])
|
||||
@@ -20,8 +20,7 @@ async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('download/')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -41,10 +40,8 @@ const filteredDataList = computed(() => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser)
|
||||
return dataList.value
|
||||
else
|
||||
return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||
if (superUser) return dataList.value
|
||||
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||
})
|
||||
|
||||
// 加载时获取数据
|
||||
@@ -67,23 +64,10 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
/>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="filteredDataList.length > 0"
|
||||
class="grid gap-3 grid-downloading-card"
|
||||
>
|
||||
<DownloadingCard
|
||||
v-for="data in filteredDataList"
|
||||
:key="data.hash"
|
||||
:info="data"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VPullToRefresh v-model="loading" @load="onRefresh" :pull-down-threshold="64">
|
||||
<div v-if="filteredDataList.length > 0" class="grid gap-3 grid-downloading-card">
|
||||
<DownloadingCard v-for="data in filteredDataList" :key="data.hash" :info="data" />
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||
@@ -91,5 +75,5 @@ onUnmounted(() => {
|
||||
error-title="没有任务"
|
||||
error-description="正在下载的任务将会显示在这里。"
|
||||
/>
|
||||
</PullRefresh>
|
||||
</VPullToRefresh>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
@@ -29,42 +30,65 @@ const endpoints = {
|
||||
},
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
// 当前目录
|
||||
const path: Ref<string | undefined> = ref()
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
function loadSystemSettings(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api
|
||||
.get('system/env')
|
||||
.then((result: any) => {
|
||||
let path = '/'
|
||||
if (result.success)
|
||||
path = result.data?.DOWNLOAD_PATH || '/'
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
if (!path.endsWith('/'))
|
||||
path += '/'
|
||||
// 计算公共路径
|
||||
function findCommonPath(paths: string[]): string {
|
||||
let commonPath = '/'
|
||||
if (!paths || paths.length === 0) {
|
||||
commonPath = '/'
|
||||
} else if (paths.length === 1) {
|
||||
commonPath = paths[0]
|
||||
commonPath = commonPath.replace(/\\/g, '/')
|
||||
} else {
|
||||
const normalizedPaths = paths.map(path => path.replace(/\\/g, '/'))
|
||||
const splitPaths = normalizedPaths.map(path => path.split('/'))
|
||||
let commonParts: string[] = []
|
||||
for (let i = 0; i < splitPaths[0].length; i++) {
|
||||
const part = splitPaths[0][i]
|
||||
if (splitPaths.every(pathParts => pathParts[i] === part)) {
|
||||
commonParts.push(part)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
commonPath = commonParts.join('/')
|
||||
}
|
||||
|
||||
resolve(path)
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
if (!commonPath.endsWith('/')) {
|
||||
commonPath += '/'
|
||||
}
|
||||
|
||||
if (commonPath.includes(':')) {
|
||||
commonPath = commonPath.replace('/', '\\')
|
||||
}
|
||||
|
||||
return commonPath
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
path.value = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 目录变化
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
.then((res) => {
|
||||
path.value = res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
path.value = '/'
|
||||
})
|
||||
})
|
||||
onBeforeMount(loadDownloadDirectories)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -339,6 +339,7 @@ onMounted(fetchData)
|
||||
show-select
|
||||
loading-text="加载中..."
|
||||
class="data-table-div"
|
||||
hover
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
@@ -413,6 +414,32 @@ onMounted(fetchData)
|
||||
</VPagination>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<span>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-16"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</span>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
<VCard class="text-center rounded-t">
|
||||
@@ -451,31 +478,6 @@ onMounted(fetchData)
|
||||
"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
<!-- 底部操作按钮 -->
|
||||
<span>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-2"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -5,12 +5,13 @@ import draggable from 'vuedraggable'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import MediaDirectoryCard from '@/components/cards/MediaDirectoryCard.vue'
|
||||
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
|
||||
|
||||
// 媒体库设置项
|
||||
const transferSettings = ref({
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
TRANSFER_SAME_DISK: true,
|
||||
})
|
||||
|
||||
// 转移方式字典
|
||||
@@ -48,10 +49,11 @@ async function loadTransferSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const { TRANSFER_TYPE, OVERWRITE_MODE } = result.data
|
||||
const { TRANSFER_TYPE, OVERWRITE_MODE, TRANSFER_SAME_DISK } = result.data
|
||||
transferSettings.value = {
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
TRANSFER_SAME_DISK,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -221,10 +223,11 @@ onMounted(() => {
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<MediaDirectoryCard
|
||||
<DirectoryCard
|
||||
type="download"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@update:modelValue="(value: string) => (element.path = value)"
|
||||
@close="downloadCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
@@ -254,7 +257,7 @@ onMounted(() => {
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<MediaDirectoryCard
|
||||
<DirectoryCard
|
||||
type="library"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@@ -296,6 +299,13 @@ onMounted(() => {
|
||||
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferSettings.TRANSFER_SAME_DISK"
|
||||
label="同盘/同根目录优先"
|
||||
hint="开启后优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -10,7 +10,7 @@ const $toast = useToast()
|
||||
const schedulerList = ref<ScheduleInfo[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载定时服务列表
|
||||
async function loadSchedulerList() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ onBeforeMount(fetchData)
|
||||
error-description="已添加并支持的站点将会在这里显示。"
|
||||
/>
|
||||
<!-- 新增站点按钮 -->
|
||||
<VFab icon="mdi-plus" location="bottom end" size="x-large" fixed app appear @click="siteAddDialog = true" />
|
||||
<VFab icon="mdi-plus" location="bottom" size="x-large" fixed app appear @click="siteAddDialog = true" />
|
||||
<!-- 新增站点弹窗 -->
|
||||
<SiteAddEditDialog
|
||||
v-if="siteAddDialog"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
@@ -60,7 +60,7 @@ const filteredDataList = computed(() => {
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<PullRefresh v-model="loading" @refresh="onRefresh">
|
||||
<VPullToRefresh v-model="loading" @load="onRefresh">
|
||||
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1">
|
||||
<SubscribeCard
|
||||
v-for="data in filteredDataList"
|
||||
@@ -76,12 +76,12 @@ const filteredDataList = computed(() => {
|
||||
error-title="没有订阅"
|
||||
error-description="请通过搜索添加电影、电视剧订阅。"
|
||||
/>
|
||||
</PullRefresh>
|
||||
</VPullToRefresh>
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-clipboard-edit"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
@@ -92,8 +92,8 @@ const filteredDataList = computed(() => {
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom end"
|
||||
class="mb-2"
|
||||
location="bottom"
|
||||
class="mb-16"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
|
||||
320
src/views/system/SearchBarView.vue
Normal file
320
src/views/system/SearchBarView.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Plugin } 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?.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?.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)
|
||||
})
|
||||
})
|
||||
|
||||
// 跳转媒体搜索页面
|
||||
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')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
searchWordInput.value?.focus()
|
||||
}, 500)
|
||||
fetchInstalledPlugins()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog max-width="40rem">
|
||||
<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="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>
|
||||
18
yarn.lock
18
yarn.lock
@@ -5255,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"
|
||||
@@ -5814,11 +5819,6 @@ psl@^1.1.28:
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
|
||||
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
|
||||
|
||||
pull-refresh-vue3@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pull-refresh-vue3/-/pull-refresh-vue3-0.3.1.tgz#e75ffad5d71e30a85b5338f2beca9fc8a1e01432"
|
||||
integrity sha512-DTcosG4LT3dGF/amzMP3YOwQ11x1taxQU3ChGx2SDCT5LMmyNpUD7pb2FAZkB/QEHVSpUTIfCcanfXANkYOXjw==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
@@ -7342,10 +7342,10 @@ vuetify-use-dialog@^0.6.11:
|
||||
dependencies:
|
||||
defu "^6.1.4"
|
||||
|
||||
vuetify@3.5.14:
|
||||
version "3.5.14"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.14.tgz#9590a06bcf49398f1303203b84cc065899d7cec8"
|
||||
integrity sha512-bmfid7K4D+wPi9h7sK4PxjmIB2tBzNuqlW14cs30iQ7GAphEeo/HYwn6aEdNK/Na+imhti8CJDDqdGs6SEfyXQ==
|
||||
vuetify@3.6.8:
|
||||
version "3.6.8"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.6.8.tgz#89ab0b68aa5488c7b54a04fa4a02a1b802892aaa"
|
||||
integrity sha512-j0v0iTeSVRj2ZEM9Q8HxejHxmxrQLYQSalhH82hfcraORaiDoqf1XV05N3P5ERXkKiJjJc/LfxFAUUvYSldxeg==
|
||||
|
||||
vuex-persistedstate@^4.1.0:
|
||||
version "4.1.0"
|
||||
|
||||
Reference in New Issue
Block a user