mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
Compare commits
31 Commits
v1.9.1-bet
...
v1.9.2-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c3563f4501 | ||
|
|
a543202edc | ||
|
|
52cf517a91 | ||
|
|
11b649dc8c | ||
|
|
19663bacb1 | ||
|
|
41c276d0e0 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.9.1-beta",
|
||||
"version": "1.9.2-3",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -101,4 +101,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -464,6 +464,8 @@ export interface DashboardItem {
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 插件的仪表板key
|
||||
key: string
|
||||
// 全局配置
|
||||
attrs: { [key: string]: any }
|
||||
// col列数
|
||||
@@ -716,12 +718,6 @@ export interface NotificationSwitch {
|
||||
vocechat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
export interface Setting {
|
||||
// 下载目录
|
||||
DOWNLOAD_PATH: string
|
||||
}
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
// 文件列表
|
||||
|
||||
@@ -22,7 +22,6 @@ const typeItems = [
|
||||
{ title: '全部', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
{ title: '动漫', value: '动漫' },
|
||||
]
|
||||
|
||||
// 定义触发的自定义事件
|
||||
|
||||
@@ -167,6 +167,12 @@ watch(resourceDialog, value => {
|
||||
if (!value) getSiteStats()
|
||||
})
|
||||
|
||||
// 保存站点
|
||||
function saveSite() {
|
||||
siteEditDialog.value = false
|
||||
emit('update')
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
@@ -175,150 +181,142 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardActions>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<div>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText class="py-2">
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="
|
||||
() => {
|
||||
siteEditDialog = false
|
||||
emit('update')
|
||||
}
|
||||
"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText class="pt-2">
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
<span class="absolute top-1 right-8">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</span>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="saveSite"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText class="pt-2">
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.v-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -20,9 +21,9 @@ const props = defineProps({
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
// 生成1到100季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||
Array.from({ length: 101 }, (_, i) => i).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
@@ -53,7 +54,7 @@ const progressValue = ref(0)
|
||||
const transferForm = reactive({
|
||||
logid: 0,
|
||||
path: '',
|
||||
target: props.target ?? '',
|
||||
target: props.target ?? null,
|
||||
tmdbid: null,
|
||||
doubanid: null,
|
||||
season: null,
|
||||
@@ -64,12 +65,32 @@ const transferForm = reactive({
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
scrape: true,
|
||||
scrape: false,
|
||||
})
|
||||
|
||||
// 所有媒体库目录
|
||||
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 目的目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
const directories = libraryDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 监听输入变化
|
||||
watchEffect(() => {
|
||||
transferForm.path = props.path ?? ''
|
||||
transferForm.target = props.target ?? ''
|
||||
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监听加载进度
|
||||
@@ -158,8 +179,21 @@ async function loadSystemSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询媒体库目录
|
||||
async function loadLibraryDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/LibraryDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
libraryDirectories.value = result.data.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
loadLibraryDirectories()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -175,11 +209,12 @@ onMounted(() => {
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="transferForm.target"
|
||||
:items="targetDirectories"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
hint="留空将自动整理到媒体库目录"
|
||||
hint="留空将自动匹配目标路径"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
|
||||
@@ -48,7 +48,7 @@ const statusItems = [
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
Array.from({ length: 100 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
import type { MediaDirectory, Site, Subscribe } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
@@ -25,6 +25,9 @@ const emit = defineEmits(['remove', 'save', 'close'])
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
@@ -167,6 +170,25 @@ async function removeSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
// 去重后的下载目录
|
||||
const directories = downloadDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 质量选择框数据
|
||||
const qualityOptions = ref([
|
||||
{
|
||||
@@ -252,9 +274,9 @@ const effectOptions = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloadDirectories()
|
||||
getSiteList()
|
||||
if (props.subid) getSubscribeInfo()
|
||||
|
||||
if (props.default) queryDefaultSubscribeConfig()
|
||||
})
|
||||
</script>
|
||||
@@ -338,8 +360,9 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="subscribeForm.save_path"
|
||||
:items="targetDirectories"
|
||||
label="保存路径"
|
||||
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,14 +85,14 @@ const superUser = store.state.auth.superUser
|
||||
:item="{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie-check-outline',
|
||||
to: '/subscribe-movie',
|
||||
to: '/subscribe-movie/mysub',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
to: '/subscribe-tv',
|
||||
to: '/subscribe-tv/mysub',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
@@ -144,7 +144,7 @@ const superUser = store.state.auth.superUser
|
||||
:item="{
|
||||
title: '插件',
|
||||
icon: 'mdi-apps',
|
||||
to: '/plugins',
|
||||
to: '/plugins/installed',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
@@ -160,7 +160,7 @@ const superUser = store.state.auth.superUser
|
||||
:item="{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
to: '/setting',
|
||||
to: '/setting/account',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -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,11 +301,14 @@ 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -54,7 +54,7 @@ function startLoadingProgress() {
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
@@ -80,34 +80,33 @@ async function fetchData() {
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
dataList.value = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
|
||||
const result: {[key: string]: any} = await api.get(`search/media/${keyword}`, {
|
||||
const result: { [key: string]: any } = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
area,
|
||||
season,
|
||||
},
|
||||
})
|
||||
if (result.success){
|
||||
if (result.success) {
|
||||
dataList.value = result.data
|
||||
} else {
|
||||
errorDescription.value = result.message
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${keyword}`)
|
||||
}
|
||||
stopLoadingProgress()
|
||||
// 从浏览器历史中删除当前搜索
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
// 标记已刷新
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
@@ -120,26 +119,15 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
:text="progressText"
|
||||
:progress="progressValue"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" :text="progressText" :progress="progressValue" />
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<div v-if="dataList.length > 0">
|
||||
<TorrentRowListView
|
||||
v-if="viewType === 'list'"
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentCardListView
|
||||
v-else
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentRowListView v-if="viewType === 'list'" :items="dataList" />
|
||||
<TorrentCardListView v-else :items="dataList" />
|
||||
</div>
|
||||
<!-- 视图切换 -->
|
||||
<VFab
|
||||
|
||||
@@ -73,7 +73,7 @@ const tabs = [
|
||||
<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 tabs" :key="item.icon" :value="item.tab" :to="'/setting/' + item.tab">
|
||||
<VIcon size="20" start :icon="item.icon" />
|
||||
{{ item.title }}
|
||||
</VTab>
|
||||
|
||||
@@ -23,7 +23,7 @@ const activeTab = ref(route.params.tab)
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in tabs" :value="item.tab" :to="'/subscribe-movie/' + item.tab">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
@@ -23,7 +23,7 @@ const activeTab = ref(route.params.tab)
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in tabs" :value="item.tab" :to="'/subscribe-tv/' + 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: [
|
||||
@@ -43,14 +42,14 @@ const router = createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-movie',
|
||||
path: 'subscribe-movie/:tab',
|
||||
component: () => import('../pages/subscribe-movie.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-tv',
|
||||
path: 'subscribe-tv/:tab',
|
||||
component: () => import('../pages/subscribe-tv.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
@@ -85,14 +84,14 @@ const router = createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
path: 'plugins/:tab',
|
||||
component: () => import('../pages/plugin.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
path: 'setting/:tab',
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -139,3 +139,52 @@
|
||||
.apexcharts-title-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-directory-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
@@ -38,9 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,10 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,10 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,7 +125,5 @@ async function fetchData({ done }: { done: any }) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -124,9 +124,3 @@ async function fetchData({ done }: { done: any }) {
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,10 +231,3 @@ watchEffect(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,11 +25,11 @@ const activeTab = ref(route.params.tab)
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的插件',
|
||||
tab: 'myplugin',
|
||||
tab: 'installed',
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
tab: 'pluginmarket',
|
||||
tab: 'market',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -330,14 +330,14 @@ onBeforeMount(async () => {
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in tabs" :value="item.tab" :to="'/plugins/' + 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 +363,7 @@ onBeforeMount(async () => {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- 插件市场 -->
|
||||
<VWindowItem value="pluginmarket">
|
||||
<VWindowItem value="market">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
@@ -516,14 +516,3 @@ onBeforeMount(async () => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -93,10 +93,3 @@ onUnmounted(() => {
|
||||
/>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,9 +19,6 @@ const currentHistory = ref<TransferHistory>()
|
||||
// 重新整理IDS
|
||||
const redoIds = ref<number[]>([])
|
||||
|
||||
// 重新整理target
|
||||
const redoTarget = ref('')
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
@@ -271,19 +268,6 @@ async function retransferBatch() {
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1) {
|
||||
// 目的目录
|
||||
const dest = selected.value[0].dest ?? ''
|
||||
// 类型
|
||||
const mediaType = selected.value[0].type ?? ''
|
||||
// 分类
|
||||
const category = selected.value[0].category ?? ''
|
||||
// 计算根路径
|
||||
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||
} else {
|
||||
redoTarget.value = ''
|
||||
}
|
||||
// 打开识别弹窗
|
||||
redoDialog.value = true
|
||||
}
|
||||
@@ -297,7 +281,6 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -356,6 +339,7 @@ onMounted(fetchData)
|
||||
show-select
|
||||
loading-text="加载中..."
|
||||
class="data-table-div"
|
||||
hover
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
@@ -456,7 +440,6 @@ onMounted(fetchData)
|
||||
v-if="redoDialog"
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="
|
||||
() => {
|
||||
redoDialog = false
|
||||
|
||||
@@ -11,6 +11,7 @@ import MediaDirectoryCard from '@/components/cards/MediaDirectoryCard.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) {
|
||||
@@ -296,6 +298,13 @@ onMounted(() => {
|
||||
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferSettings.TRANSFER_SAME_DISK"
|
||||
label="同盘/同根目录优先"
|
||||
hint="开启后优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -310,9 +319,3 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.grid-directory-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -440,10 +440,3 @@ onMounted(() => {
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -550,10 +550,3 @@ onMounted(() => {
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
@@ -24,16 +25,40 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存站点排序
|
||||
async function savaSitesPriority() {
|
||||
// 重新排序
|
||||
const priorities = dataList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||
if (result.success) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-site-card">
|
||||
<div v-for="(data, index) in dataList" :key="index">
|
||||
<SiteCard :key="data.id" :site="data" @remove="fetchData" @update="fetchData" />
|
||||
</div>
|
||||
<div>
|
||||
<draggable
|
||||
v-if="dataList.length > 0"
|
||||
v-model="dataList"
|
||||
@end="savaSitesPriority"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-site-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SiteCard :site="element" @remove="fetchData" @update="fetchData" />
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
@@ -57,10 +82,3 @@ onBeforeMount(fetchData)
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -123,10 +123,3 @@ const filteredDataList = computed(() => {
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -129,13 +129,3 @@ async function fetchData({ done }: { done: any }) {
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user