mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72cb797ab | ||
|
|
6898e6b816 | ||
|
|
adb0b966ff | ||
|
|
81284b8d21 | ||
|
|
1a2b112e64 | ||
|
|
442c484dc9 | ||
|
|
2368c2f25f | ||
|
|
2320c58254 | ||
|
|
c9a4f36414 | ||
|
|
1df9a981b2 | ||
|
|
cbc917b834 | ||
|
|
240a568d16 | ||
|
|
eb1a847faa | ||
|
|
e09e57879b | ||
|
|
ddd2982971 | ||
|
|
621da7e4ef | ||
|
|
420827c389 | ||
|
|
ce9399b894 | ||
|
|
1bdd08c59a | ||
|
|
52d62dda81 | ||
|
|
d69e3cedae | ||
|
|
648bfcdd0d | ||
|
|
e4b8ff0a64 | ||
|
|
4576ef854d | ||
|
|
7323668db5 | ||
|
|
b11d709070 | ||
|
|
e25ac006c2 | ||
|
|
9e85e7edce | ||
|
|
804bcd440c | ||
|
|
6e4c896cb7 | ||
|
|
0cae89f8e3 | ||
|
|
59a7607c07 | ||
|
|
6dca0c157f |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.4",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -32,6 +32,7 @@
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.6.8",
|
||||
"colorthief": "^2.4.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
|
||||
@@ -115,7 +115,7 @@ function changeTheme(theme: string) {
|
||||
// 保存主题到服务端
|
||||
try {
|
||||
api.post('/user/config/Layout', {
|
||||
theme: nextTheme
|
||||
theme: nextTheme,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('保存主题到服务端失败')
|
||||
@@ -176,7 +176,7 @@ async function saveCustomCSS() {
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) $toast.success('自定义CSS保存成功!')
|
||||
if (result.success) $toast.success('自定义CSS保存成功,请刷新页面生效!')
|
||||
} catch (e) {
|
||||
console.error('保存自定义 CSS 到服务端失败')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
// 请求和获取剪贴板内容
|
||||
export async function getClipboardContent() {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
@@ -13,20 +15,10 @@ export async function getClipboardContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// 将内容复制到剪贴板,兼容非安全域场景
|
||||
// 将内容复制到剪贴板
|
||||
export async function copyToClipboard(content: string) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(content)
|
||||
} else {
|
||||
const input = document.createElement('textarea')
|
||||
input.value = content
|
||||
document.body.appendChild(input)
|
||||
// 阻止事件冒泡到其他元素,确保 focusin 事件只在 textarea 元素上处理,不会影响其他元素
|
||||
input.addEventListener('focusin', e => e.stopPropagation())
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
const success = copy(content)
|
||||
return success
|
||||
}
|
||||
|
||||
// VAPID公钥转Uint8Array
|
||||
|
||||
@@ -98,8 +98,8 @@ function onClose() {
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start">
|
||||
<h5 class="text-h6 mb-1">{{ props.rule.id }}</h5>
|
||||
<div class="text-body-1 mb-3">{{ props.rule.name }}</div>
|
||||
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
|
||||
<div class="text-body-1 mb-3">{{ props.rule.id }}</div>
|
||||
</div>
|
||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
||||
</VCardText>
|
||||
@@ -174,9 +174,9 @@ function onClose() {
|
||||
<VCol cols="6">
|
||||
<VTextField
|
||||
v-model="ruleInfo.publish_time"
|
||||
placeholder="0"
|
||||
placeholder="0/1-10"
|
||||
label="发布时间(分钟)"
|
||||
hint="距离资源发布的最小时间间隔"
|
||||
hint="距离资源发布的最小时间间隔或时间区间"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
|
||||
@@ -101,7 +101,7 @@ function filterCardClose(pri: string) {
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules() {
|
||||
async function shareRules() {
|
||||
if (filterRuleCards.value.length === 0) return
|
||||
|
||||
const value = filterRuleCards.value
|
||||
@@ -110,8 +110,10 @@ function shareRules() {
|
||||
.join('>')
|
||||
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
let success
|
||||
success = copyToClipboard(value)
|
||||
if (await success) $toast.success('优先级规则已复制到剪贴板!')
|
||||
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
console.error(error)
|
||||
|
||||
@@ -149,80 +149,82 @@ const dropdownItems = ref([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
||||
<div class="me-n3 absolute bottom-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div>
|
||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col h-full">
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 安装插件进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 安装插件进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -390,130 +390,146 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 插件卡片 -->
|
||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
||||
<div class="me-n3 absolute bottom-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
/>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
<div>
|
||||
<!-- 插件卡片 -->
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-if="isVisible"
|
||||
v-bind="hover.props"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="openPluginDetail"
|
||||
class="flex flex-col h-full"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
/>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span class="author-info">
|
||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
||||
</VImg>
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
|
||||
</div>
|
||||
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span class="author-info">
|
||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
||||
</VImg>
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
</VCardText>
|
||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
</div>
|
||||
</VCard>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
||||
<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" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
||||
<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" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VBtn @click="updatePlugin" block>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||
</template>
|
||||
更新到最新版本
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VBtn @click="updatePlugin" block>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||
</template>
|
||||
更新到最新版本
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -116,7 +116,7 @@ async function resetSubscribe() {
|
||||
try {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
|
||||
content: `重置后 ${props.media?.name} 将恢复初始状态,已下载记录将被清除,未入库的内容将会重新下载,是否确认?`,
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
// 重置
|
||||
@@ -124,6 +124,7 @@ async function resetSubscribe() {
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.name} 重置成功!`)
|
||||
subscribeState.value = 'R'
|
||||
emit('save')
|
||||
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
|
||||
} catch (e) {
|
||||
@@ -208,7 +209,6 @@ const dropdownItems = computed(() => [
|
||||
click: resetSubscribe,
|
||||
color: 'warning',
|
||||
},
|
||||
show: props.media?.type === '电视剧',
|
||||
},
|
||||
{
|
||||
title: '分享',
|
||||
@@ -271,134 +271,136 @@ function onSubscribeEditRemove() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col rounded-lg"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-2">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem
|
||||
v-if="item.show !== false"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div
|
||||
v-if="subscribeState === 'P'"
|
||||
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
|
||||
></div>
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
<div>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col rounded-lg h-full"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-2">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem
|
||||
v-if="item.show !== false"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="onSubscribeEditRemove"
|
||||
@save="onSubscribeEditSave"
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
||||
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="onSubscribeEditRemove"
|
||||
@save="onSubscribeEditSave"
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
|
||||
<!-- 订阅文件信息弹窗 -->
|
||||
<SubscribeFilesDialog
|
||||
v-if="subscribeFilesDialog"
|
||||
v-model="subscribeFilesDialog"
|
||||
:subid="props.media?.id"
|
||||
@close="subscribeFilesDialog = false"
|
||||
/>
|
||||
<!-- 分享订阅弹窗 -->
|
||||
<SubscribeShareDialog
|
||||
v-if="subscribeShareDialog"
|
||||
v-model="subscribeShareDialog"
|
||||
:sub="props.media"
|
||||
@close="subscribeShareDialog = false"
|
||||
/>
|
||||
<!-- 订阅文件信息弹窗 -->
|
||||
<SubscribeFilesDialog
|
||||
v-if="subscribeFilesDialog"
|
||||
v-model="subscribeFilesDialog"
|
||||
:subid="props.media?.id"
|
||||
@close="subscribeFilesDialog = false"
|
||||
/>
|
||||
<!-- 分享订阅弹窗 -->
|
||||
<SubscribeShareDialog
|
||||
v-if="subscribeShareDialog"
|
||||
v-model="subscribeShareDialog"
|
||||
:sub="props.media"
|
||||
@close="subscribeShareDialog = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.subscribe-card-background {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
@@ -28,6 +29,9 @@ const pluginId = ref(route.query.id)
|
||||
// 当前排序字段
|
||||
const activeSort = ref(null)
|
||||
|
||||
// 插件顺序配置
|
||||
const orderConfig = ref<{ id: string }[]>([])
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = [
|
||||
{ title: '热门', value: 'count' },
|
||||
@@ -104,6 +108,52 @@ const labelFilterOptions = ref<string[]>([])
|
||||
// 插件库过滤项
|
||||
const repoFilterOptions = ref<string[]>([])
|
||||
|
||||
// 加载插件顺序
|
||||
async function loadPluginOrderConfig() {
|
||||
// 顺序配置
|
||||
const local_order = localStorage.getItem('MP_PLUGIN_ORDER')
|
||||
if (local_order) {
|
||||
orderConfig.value = JSON.parse(local_order)
|
||||
} else {
|
||||
const response2 = await api.get('/user/config/PluginOrder')
|
||||
if (response2 && response2.data && response2.data.value) {
|
||||
orderConfig.value = response2.data.value
|
||||
localStorage.setItem('MP_PLUGIN_ORDER', JSON.stringify(orderConfig.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按order的顺序对插件进行排序
|
||||
function sortPluginOrder() {
|
||||
if (!orderConfig.value) {
|
||||
return
|
||||
}
|
||||
if (dataList.value.length === 0) {
|
||||
return
|
||||
}
|
||||
dataList.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)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 保存顺序设置
|
||||
async function savePluginOrder() {
|
||||
// 顺序配置
|
||||
const orderObj = dataList.value.map(item => ({ id: item.id || '' }))
|
||||
orderConfig.value = orderObj
|
||||
const orderString = JSON.stringify(orderObj)
|
||||
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
|
||||
|
||||
// 保存到服务端
|
||||
try {
|
||||
await api.post('/user/config/PluginOrder', orderObj)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化过滤选项
|
||||
function initOptions(item: Plugin) {
|
||||
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||
@@ -208,6 +258,8 @@ async function fetchInstalledPlugins() {
|
||||
state: 'installed',
|
||||
},
|
||||
})
|
||||
// 排序
|
||||
sortPluginOrder()
|
||||
loading.value = false
|
||||
isRefreshed.value = true
|
||||
} catch (error) {
|
||||
@@ -329,8 +381,9 @@ function handleRepoUrl(url: string | undefined) {
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(async () => {
|
||||
await refreshData()
|
||||
onMounted(async () => {
|
||||
await loadPluginOrderConfig()
|
||||
refreshData()
|
||||
getPluginStatistics()
|
||||
if (activeTab.value != 'market' && pluginId.value) {
|
||||
// 找到这个插件
|
||||
@@ -356,18 +409,26 @@ onBeforeMount(async () => {
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card">
|
||||
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<draggable
|
||||
v-if="dataList.length > 0"
|
||||
v-model="dataList"
|
||||
@end="savePluginOrder"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ class: 'grid gap-4 grid-plugin-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<PluginCard
|
||||
:count="PluginStatistics[data.id || '0']"
|
||||
:plugin="data"
|
||||
:action="pluginActions[data.id || '0']"
|
||||
:count="PluginStatistics[element.id || '0']"
|
||||
:plugin="element"
|
||||
:action="pluginActions[element.id || '0']"
|
||||
@remove="refreshData"
|
||||
@save="refreshData"
|
||||
@action-done="pluginActions[data.id || '0'] = false"
|
||||
@action-done="pluginActions[element.id || '0'] = false"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</draggable>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
|
||||
@@ -8,41 +8,12 @@ import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
||||
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
import internal from 'stream'
|
||||
|
||||
// 自定义规则列表
|
||||
const customRules = ref<CustomRule[]>([])
|
||||
// 自定义规则类型,仅用于导入判断
|
||||
const customRulesType = ref<CustomRule>({
|
||||
// 规则ID
|
||||
id: '',
|
||||
// 名称
|
||||
name: '',
|
||||
// 包含
|
||||
include: '',
|
||||
// 排除
|
||||
exclude: '',
|
||||
// 大小范围
|
||||
size_range: '',
|
||||
// 最少做种人数
|
||||
seeders: '',
|
||||
// 发布时间
|
||||
publish_time: '',
|
||||
})
|
||||
|
||||
// 所有规则组列表
|
||||
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
||||
// 规则组类型,仅用于导入判断
|
||||
const filterRuleGroupsType = ref<FilterRuleGroup>({
|
||||
// 名称
|
||||
name: '',
|
||||
// 规则串
|
||||
rule_string: '',
|
||||
// 适用类媒体类型 None-全部 电影/电视剧
|
||||
media_type: '',
|
||||
// # 适用媒体类别 None-全部 对应二级分类
|
||||
category: '',
|
||||
})
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
@@ -123,8 +94,6 @@ async function addCustomRule() {
|
||||
customRules.value.push({
|
||||
id: id,
|
||||
name: name,
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,14 +143,11 @@ function addFilterRuleGroup() {
|
||||
}
|
||||
filterRuleGroups.value.push({
|
||||
name: name,
|
||||
rule_string: '',
|
||||
media_type: '',
|
||||
category: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
|
||||
async function shareRules(rules: CustomRule[] | FilterRuleGroup[], type: string) {
|
||||
if (!rules || rules.length === 0) return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
@@ -189,10 +155,13 @@ function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
let success
|
||||
success = copyToClipboard(value)
|
||||
if (await success) $toast.success(`${type === 'custom' ? '自定义规则' : '优先级规则组'}已复制到剪贴板!`)
|
||||
else $toast.error(`${type === 'custom' ? '自定义规则' : '优先级规则组'}复制失败:可能是浏览器不支持或被用户阻止!`)
|
||||
} catch (e) {
|
||||
$toast.error(`${type === 'custom' ? '自定义规则' : '优先级规则组'}复制失败!`)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,15 +186,11 @@ function saveCodeString(type: string, codeString: any) {
|
||||
// 更新数据
|
||||
try {
|
||||
if (type === 'custom') {
|
||||
for (const value of parsedCode) {
|
||||
if (!validateValueAgainstInterface(value, customRulesType)) return false
|
||||
}
|
||||
if (!checkValueValidity(parsedCode, type)) return false
|
||||
const newCustomRules = extractCustomRules(parsedCode) || []
|
||||
customRules.value = [...customRules.value, ...newCustomRules]
|
||||
} else if (type === 'group') {
|
||||
for (const value of parsedCode) {
|
||||
if (!validateValueAgainstInterface(value, filterRuleGroupsType)) return false
|
||||
}
|
||||
if (!checkValueValidity(parsedCode, type)) return false
|
||||
const newFilterRuleGroups = extractFilterRuleGroups(parsedCode) || []
|
||||
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups]
|
||||
} else {
|
||||
@@ -246,6 +211,9 @@ function extractCustomRules(value: any) {
|
||||
name: item.name,
|
||||
include: item.include,
|
||||
exclude: item.exclude,
|
||||
size_range: item.size_range,
|
||||
seeders: item.seeders,
|
||||
publish_time: item.publish_time,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -269,14 +237,33 @@ function extractFilterRuleGroups(value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateValueAgainstInterface(value: any, interfaceDefinition: any): boolean {
|
||||
console.log(value)
|
||||
console.log(interfaceDefinition)
|
||||
// 根据ID简单区分规则与规则组
|
||||
function checkValueValidity(values: any, type: string): boolean {
|
||||
try {
|
||||
// 循环判断是否命中全部接口定义,暂时只检查是否存在,不检查是否多出
|
||||
for (const key in interfaceDefinition.value) {
|
||||
if (value[key] === undefined) {
|
||||
$toast.error(`导入规则失败!输入了不符合要求的数据!`)
|
||||
// 允许空值存在,不影响最终的导入
|
||||
if (!values) return true
|
||||
if (!type) return false
|
||||
for (const value of values) {
|
||||
const keys = Object.keys(value)
|
||||
const uniqueKeys = new Set(keys)
|
||||
|
||||
const hasName = keys.includes('name')
|
||||
const hasId = keys.includes('id')
|
||||
const noDuplicates = keys.length === uniqueKeys.size
|
||||
if (type == 'custom') {
|
||||
if (!hasName || !hasId || !noDuplicates) {
|
||||
if (!noDuplicates) $toast.warning(`存在重名值`)
|
||||
if (!hasId) $toast.error(`导入失败!发现有规则不存在ID,可能属于优先级规则组!`)
|
||||
return false
|
||||
}
|
||||
} else if (type == 'group') {
|
||||
if (!hasName || hasId || !noDuplicates) {
|
||||
if (!noDuplicates) $toast.warning(`存在重名值`)
|
||||
if (hasId) $toast.error(`导入失败!发现有规则存在ID,可能属于自定义规则!`)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
console.error(`传入了不合法的类型!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -287,6 +274,18 @@ function validateValueAgainstInterface(value: any, interfaceDefinition: any): bo
|
||||
}
|
||||
}
|
||||
|
||||
// 清空规则(组)
|
||||
function deleteAllRules(dateType: string) {
|
||||
if (!dateType) return
|
||||
if (dateType === 'custom') {
|
||||
customRules.value = []
|
||||
} else if (dateType === 'group') {
|
||||
filterRuleGroups.value = []
|
||||
} else {
|
||||
console.error(`传入了不支持的类型!`)
|
||||
}
|
||||
}
|
||||
|
||||
// 规则变化时赋值
|
||||
function onRuleChange(rule: CustomRule, id: string) {
|
||||
const index = customRules.value.findIndex(item => item.id === id)
|
||||
@@ -383,12 +382,15 @@ onMounted(() => {
|
||||
<VBtn color="success" variant="tonal" @click="addCustomRule">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="importRules('custom')">
|
||||
<VBtn color="primary" variant="tonal" @click="importRules('custom')">
|
||||
<VIcon icon="mdi-import" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="shareRules(customRules)">
|
||||
<VBtn color="info" variant="tonal" @click="shareRules(customRules, 'custom')">
|
||||
<VIcon icon="mdi-share" />
|
||||
</VBtn>
|
||||
<VBtn color="error" variant="tonal" @click="deleteAllRules('custom')">
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</VBtnGroup>
|
||||
</div>
|
||||
</VForm>
|
||||
@@ -431,12 +433,15 @@ onMounted(() => {
|
||||
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="importRules('group')">
|
||||
<VBtn color="primary" variant="tonal" @click="importRules('group')">
|
||||
<VIcon icon="mdi-import" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups)">
|
||||
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups, 'group')">
|
||||
<VIcon icon="mdi-share" />
|
||||
</VBtn>
|
||||
<VBtn color="error" variant="tonal" @click="deleteAllRules('group')">
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</VBtnGroup>
|
||||
</div>
|
||||
</VForm>
|
||||
@@ -447,7 +452,7 @@ onMounted(() => {
|
||||
<ImportCodeDialog
|
||||
v-if="importCodeDialog"
|
||||
v-model="importCodeDialog"
|
||||
:title="`导入${importCodeType === 'custom' ? '自定义规则' : '规则组'}`"
|
||||
:title="`导入${importCodeType === 'custom' ? '自定义规则' : '优先级规则组'}`"
|
||||
:dataType="importCodeType"
|
||||
@close="importCodeDialog = false"
|
||||
@save="saveCodeString"
|
||||
|
||||
@@ -200,10 +200,12 @@ async function saveAdvancedSettings() {
|
||||
}
|
||||
|
||||
// 快捷复制到剪贴板
|
||||
function copyValue(value: string) {
|
||||
async function copyValue(value: string) {
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('已复制到剪贴板')
|
||||
let success
|
||||
success = copyToClipboard(value)
|
||||
if (await success) $toast.success('已复制到剪贴板!')
|
||||
else $toast.error(`复制失败:可能是浏览器不支持或被用户阻止!`)
|
||||
} catch (error) {
|
||||
$toast.error('复制失败!')
|
||||
console.log(error)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
@@ -31,6 +32,52 @@ const subscribeEditDialog = ref(false)
|
||||
// 历史记录弹窗
|
||||
const historyDialog = ref(false)
|
||||
|
||||
// 订阅顺序配置
|
||||
const orderConfig = ref<{ id: number }[]>([])
|
||||
|
||||
// 加载顺序
|
||||
async function loadSubscribeOrderConfig() {
|
||||
// 顺序配置
|
||||
const local_order = localStorage.getItem('MP_SUBSCRIBE_ORDER')
|
||||
if (local_order) {
|
||||
orderConfig.value = JSON.parse(local_order)
|
||||
} else {
|
||||
const response2 = await api.get('/user/config/SubscribeOrder')
|
||||
if (response2 && response2.data && response2.data.value) {
|
||||
orderConfig.value = response2.data.value
|
||||
localStorage.setItem('MP_SUBSCRIBE_ORDER', JSON.stringify(orderConfig.value))
|
||||
}
|
||||
}
|
||||
// 排序
|
||||
if (orderConfig.value) {
|
||||
sortSubscribeOrder()
|
||||
}
|
||||
}
|
||||
|
||||
// 按order的顺序排序
|
||||
function sortSubscribeOrder() {
|
||||
displayList.value.sort((a, b) => {
|
||||
const aIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === a.id)
|
||||
const bIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === b.id)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 保存顺序设置
|
||||
async function saveSubscribeOrder() {
|
||||
// 顺序配置
|
||||
const orderObj = displayList.value.map(item => ({ id: item.id }))
|
||||
const orderString = JSON.stringify(orderObj)
|
||||
localStorage.setItem('MP_SUBSCRIBE_ORDER', orderString)
|
||||
|
||||
// 保存到服务端
|
||||
try {
|
||||
await api.post('/user/config/SubscribeOrder', orderObj)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
@@ -49,20 +96,28 @@ const loading = ref(false)
|
||||
// 下拉刷新
|
||||
async function onRefresh({ done }: { done: any }) {
|
||||
await fetchData()
|
||||
await loadSubscribeOrderConfig()
|
||||
done('ok')
|
||||
}
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
// 显示的订阅列表
|
||||
const displayList = ref<Subscribe[]>([])
|
||||
|
||||
// 监听dataList变化,同步更新displayList
|
||||
watch(dataList, () => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser) return dataList.value.filter(data => data.type === props.type)
|
||||
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
if (superUser) displayList.value = dataList.value.filter(data => data.type === props.type)
|
||||
else displayList.value = dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
})
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
await loadSubscribeOrderConfig()
|
||||
if (props.subid) {
|
||||
// 找到这个订阅
|
||||
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
|
||||
@@ -75,7 +130,8 @@ onMounted(async () => {
|
||||
|
||||
onActivated(async () => {
|
||||
if (!loading.value) {
|
||||
fetchData()
|
||||
await fetchData()
|
||||
await loadSubscribeOrderConfig()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -83,17 +139,21 @@ onActivated(async () => {
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<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"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
@remove="fetchData"
|
||||
@save="fetchData"
|
||||
/>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="displayList.length > 0"
|
||||
v-model="displayList"
|
||||
@end="saveSubscribeOrder"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ class: 'mx-3 grid gap-4 grid-subscribe-card p-1' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
|
||||
</template>
|
||||
</draggable>
|
||||
<NoDataFound
|
||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||
v-if="displayList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有订阅"
|
||||
error-description="请通过搜索添加电影、电视剧订阅。"
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -3101,6 +3101,13 @@ cookie@0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||
|
||||
copy-to-clipboard@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
|
||||
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
core-js-compat@^3.31.0, core-js-compat@^3.36.1:
|
||||
version "3.37.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee"
|
||||
@@ -7086,6 +7093,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
toggle-selection@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||
|
||||
toidentifier@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
|
||||
Reference in New Issue
Block a user