mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 17:26:41 +08:00
Merge branch 'jxxghp:v2' into v2
This commit is contained in:
@@ -21,6 +21,8 @@ export async function copyToClipboard(content: string) {
|
|||||||
const input = document.createElement('textarea')
|
const input = document.createElement('textarea')
|
||||||
input.value = content
|
input.value = content
|
||||||
document.body.appendChild(input)
|
document.body.appendChild(input)
|
||||||
|
// 阻止事件冒泡到其他元素,确保 focusin 事件只在 textarea 元素上处理,不会影响其他元素
|
||||||
|
input.addEventListener('focusin', e => e.stopPropagation())
|
||||||
input.select()
|
input.select()
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
document.body.removeChild(input)
|
document.body.removeChild(input)
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ const pluginInfoDialog = ref(false)
|
|||||||
// 进度框文本
|
// 进度框文本
|
||||||
const progressText = ref('正在更新插件...')
|
const progressText = ref('正在更新插件...')
|
||||||
|
|
||||||
|
// 用户头像是否加载完成
|
||||||
|
const isAvatarLoaded = ref(false)
|
||||||
|
|
||||||
// 插件数据页面配置项
|
// 插件数据页面配置项
|
||||||
let pluginPageItems = ref([])
|
let pluginPageItems = ref([])
|
||||||
|
|
||||||
@@ -215,6 +218,14 @@ const iconPath: Ref<string> = computed(() => {
|
|||||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 插件作者头像路径
|
||||||
|
const authorPath: Ref<string> = computed(() => {
|
||||||
|
// 网络图片则使用代理后返回
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(
|
||||||
|
props.plugin?.author_url + '.png',
|
||||||
|
)}`
|
||||||
|
})
|
||||||
|
|
||||||
// 重置插件
|
// 重置插件
|
||||||
async function resetPlugin() {
|
async function resetPlugin() {
|
||||||
const isConfirmed = await createConfirm({
|
const isConfirmed = await createConfirm({
|
||||||
@@ -436,8 +447,10 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
<span>
|
<span class="author-info">
|
||||||
<VIcon icon="mdi-github" class="me-1" />
|
<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>
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
{{ props.plugin?.plugin_author }}
|
{{ props.plugin?.plugin_author }}
|
||||||
</a>
|
</a>
|
||||||
@@ -513,4 +526,17 @@ watch(
|
|||||||
content: '';
|
content: '';
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.author-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
block-size: 24px;
|
||||||
|
inline-size: 24px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+41
-22
@@ -31,13 +31,9 @@ const isPasswordVisible = ref(false)
|
|||||||
// 错误信息
|
// 错误信息
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
// 背景图片
|
// 背景图片 URL 和预加载 URL
|
||||||
const backgroundImageUrl = ref('')
|
const backgroundImageUrl = ref('')
|
||||||
|
|
||||||
// 所有的背景图片
|
|
||||||
const backgroundImages = ref<string[]>([])
|
const backgroundImages = ref<string[]>([])
|
||||||
|
|
||||||
// 背景图片加载状态
|
|
||||||
const isImageLoaded = ref(false)
|
const isImageLoaded = ref(false)
|
||||||
|
|
||||||
// 是否开启双重验证
|
// 是否开启双重验证
|
||||||
@@ -54,7 +50,6 @@ async function fetchBackgroundImage() {
|
|||||||
try {
|
try {
|
||||||
backgroundImages.value = await api.get('/login/wallpapers')
|
backgroundImages.value = await api.get('/login/wallpapers')
|
||||||
if (backgroundImages.value && backgroundImages.value.length > 0) {
|
if (backgroundImages.value && backgroundImages.value.length > 0) {
|
||||||
// 随机打乱排序
|
|
||||||
backgroundImages.value.sort(() => Math.random() - 0.5)
|
backgroundImages.value.sort(() => Math.random() - 0.5)
|
||||||
backgroundImageUrl.value = backgroundImages.value[0]
|
backgroundImageUrl.value = backgroundImages.value[0]
|
||||||
}
|
}
|
||||||
@@ -63,6 +58,34 @@ async function fetchBackgroundImage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换背景图片函数
|
||||||
|
function startBackgroundImageRotation() {
|
||||||
|
let currentIndex = 0
|
||||||
|
|
||||||
|
intervalTimer = setInterval(() => {
|
||||||
|
if (backgroundImages.value.length > 1) {
|
||||||
|
// 更新下一张图片索引
|
||||||
|
const nextIndex = (currentIndex + 1) % backgroundImages.value.length
|
||||||
|
const nextImageUrl = backgroundImages.value[nextIndex]
|
||||||
|
// 使用预加载机制确保下一张图片已加载完成
|
||||||
|
const img = new Image()
|
||||||
|
img.src = nextImageUrl
|
||||||
|
img.onload = () => {
|
||||||
|
// 开始淡入过渡
|
||||||
|
isImageLoaded.value = false
|
||||||
|
// 延迟一小段时间触发淡入效果
|
||||||
|
setTimeout(() => {
|
||||||
|
// 更新当前索引并切换背景图片 URL
|
||||||
|
currentIndex = nextIndex
|
||||||
|
backgroundImageUrl.value = nextImageUrl
|
||||||
|
// 切换完成后显示图片
|
||||||
|
isImageLoaded.value = true
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
// 查询是否开启双重验证
|
// 查询是否开启双重验证
|
||||||
const fetchOTP = debounce(async () => {
|
const fetchOTP = debounce(async () => {
|
||||||
const userid = usernameInput.value?.value
|
const userid = usernameInput.value?.value
|
||||||
@@ -205,14 +228,8 @@ onMounted(async () => {
|
|||||||
} else {
|
} else {
|
||||||
// 获取背景图片
|
// 获取背景图片
|
||||||
await fetchBackgroundImage()
|
await fetchBackgroundImage()
|
||||||
|
// 开始背景图片定时切换
|
||||||
// 每隔5秒更换一次背景图片
|
startBackgroundImageRotation()
|
||||||
intervalTimer = setInterval(() => {
|
|
||||||
if (backgroundImages.value.length > 0) {
|
|
||||||
const index = Math.floor(Math.random() * backgroundImages.value.length)
|
|
||||||
backgroundImageUrl.value = backgroundImages.value[index]
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -222,19 +239,21 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-for="image in backgroundImages">
|
<!-- 当前背景图片 -->
|
||||||
<div v-if="backgroundImageUrl == image" class="absolute inset-0">
|
<div class="absolute inset-0 w-full h-full overflow-hidden">
|
||||||
<VImg :src="image" class="w-full h-full" cover position="center top" @load="isImageLoaded = true">
|
<VImg
|
||||||
<template #placeholder>
|
:src="backgroundImageUrl"
|
||||||
<VSkeletonLoader v-if="!isImageLoaded" class="object-cover" />
|
:class="{ 'opacity-0': !isImageLoaded, 'opacity-100': isImageLoaded }"
|
||||||
</template>
|
class="absolute inset-0 transition-opacity duration-1000"
|
||||||
|
cover
|
||||||
|
position="center top"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
style="background-image: linear-gradient(rgba(45, 55, 72, 33%) 0%, rgb(26, 32, 46) 100%)"
|
style="background-image: linear-gradient(rgba(45, 55, 72, 33%) 0%, rgb(26, 32, 46) 100%)"
|
||||||
/>
|
/>
|
||||||
</VImg>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
||||||
<VCard
|
<VCard
|
||||||
class="auth-card px-7 py-3 w-full h-full rounded-lg"
|
class="auth-card px-7 py-3 w-full h-full rounded-lg"
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ async function loadLatest(server: string) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadMediaServerSetting()
|
await loadMediaServerSetting()
|
||||||
for (const server of mediaServers.value) {
|
const enabledServers = mediaServers.value.filter(server => server.enabled)
|
||||||
|
for (const server of enabledServers) {
|
||||||
loadLatest(server.name)
|
loadLatest(server.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ async function loadLibrary(server: string) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadMediaServerSetting()
|
await loadMediaServerSetting()
|
||||||
for (const server of mediaServers.value) {
|
const enabledServers = mediaServers.value.filter(server => server.enabled)
|
||||||
|
for (const server of enabledServers) {
|
||||||
loadLibrary(server.name)
|
loadLibrary(server.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ async function loadPlayingList(server: string) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadMediaServerSetting()
|
await loadMediaServerSetting()
|
||||||
for (const server of mediaServers.value) {
|
const enabledServers = mediaServers.value.filter(server => server.enabled)
|
||||||
|
for (const server of enabledServers) {
|
||||||
loadPlayingList(server.name)
|
loadPlayingList(server.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boo
|
|||||||
data: item,
|
data: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) $toast.error(`删除失败: ${result.msg}`)
|
if (!result.success) $toast.error(`删除失败: ${result.message}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||||
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
||||||
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
||||||
|
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||||
|
|
||||||
// 自定义规则列表
|
// 自定义规则列表
|
||||||
const customRules = ref<CustomRule[]>([])
|
const customRules = ref<CustomRule[]>([])
|
||||||
@@ -20,6 +22,15 @@ const selectedTorrentPriority = ref<string>('seeder')
|
|||||||
// 二级分类策略
|
// 二级分类策略
|
||||||
const mediaCategories = ref<{ [key: string]: any }>({})
|
const mediaCategories = ref<{ [key: string]: any }>({})
|
||||||
|
|
||||||
|
// 导入代码弹窗
|
||||||
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
|
// 导入的代码
|
||||||
|
const importCodeString = ref('')
|
||||||
|
|
||||||
|
// 导入代码类型
|
||||||
|
const importCodeType = ref('')
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -110,6 +121,63 @@ function addFilterRuleGroup() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分享规则
|
||||||
|
function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
|
||||||
|
if (!rules || rules.length === 0) return
|
||||||
|
|
||||||
|
// 将卡片规则接装为字符串
|
||||||
|
const value = JSON.stringify(rules)
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
try {
|
||||||
|
copyToClipboard(value)
|
||||||
|
$toast.success('优先级规则已复制到剪贴板')
|
||||||
|
} catch (error) {
|
||||||
|
$toast.error('优先级规则复制失败!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入规则
|
||||||
|
async function importRules(ruleType: string) {
|
||||||
|
importCodeType.value = ruleType
|
||||||
|
importCodeString.value = ''
|
||||||
|
importCodeDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听导入代码变化
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!importCodeString.value) return
|
||||||
|
// 导入代码需要json格式
|
||||||
|
try {
|
||||||
|
if (importCodeType.value === 'custom') {
|
||||||
|
// 将导入的代码转换为规则卡片
|
||||||
|
customRules.value = JSON.parse(importCodeString.value).map((item: any) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
include: item.include,
|
||||||
|
exclude: item.exclude,
|
||||||
|
publish_time: item.publish_time,
|
||||||
|
seeders: item.seeders,
|
||||||
|
size_range: item.size_range,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (importCodeType.value === 'group') {
|
||||||
|
// 将导入的代码转换为规则卡片
|
||||||
|
filterRuleGroups.value = JSON.parse(importCodeString.value).map((item: any) => {
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
rule_string: item.rule_string,
|
||||||
|
media_type: item.media_type,
|
||||||
|
category: item.category,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
$toast.error('规则导入失败!')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 规则变化时赋值
|
// 规则变化时赋值
|
||||||
function onRuleChange(rule: CustomRule) {
|
function onRuleChange(rule: CustomRule) {
|
||||||
const index = customRules.value.findIndex(item => item.id === rule.id)
|
const index = customRules.value.findIndex(item => item.id === rule.id)
|
||||||
@@ -202,9 +270,17 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VBtn type="submit" class="me-2" @click="saveCustomRules"> 保存 </VBtn>
|
<VBtn type="submit" class="me-2" @click="saveCustomRules"> 保存 </VBtn>
|
||||||
|
<VBtnGroup density="comfortable">
|
||||||
<VBtn color="success" variant="tonal" @click="addCustomRule">
|
<VBtn color="success" variant="tonal" @click="addCustomRule">
|
||||||
<VIcon icon="mdi-plus" />
|
<VIcon icon="mdi-plus" />
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
<VBtn color="info" variant="tonal" @click="importRules('custom')">
|
||||||
|
<VIcon icon="mdi-import" />
|
||||||
|
</VBtn>
|
||||||
|
<VBtn color="info" variant="tonal" @click="shareRules(customRules)">
|
||||||
|
<VIcon icon="mdi-share" />
|
||||||
|
</VBtn>
|
||||||
|
</VBtnGroup>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -236,10 +312,21 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> 保存 </VBtn>
|
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> 保存 </VBtn>
|
||||||
|
<VBtnGroup density="comfortable">
|
||||||
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
|
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
|
||||||
<VIcon icon="mdi-plus" />
|
<VIcon icon="mdi-plus" />
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
<VBtn color="info" variant="tonal" @click="importRules('group')">
|
||||||
|
<VIcon icon="mdi-import" />
|
||||||
|
</VBtn>
|
||||||
|
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups)">
|
||||||
|
<VIcon icon="mdi-share" />
|
||||||
|
</VBtn>
|
||||||
|
</VBtnGroup>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
||||||
|
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||||
|
</VDialog>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async function queryFilterRuleGroups() {
|
|||||||
|
|
||||||
// 调用API识别
|
// 调用API识别
|
||||||
async function ruleTest() {
|
async function ruleTest() {
|
||||||
if (!ruleTestForm.title) return
|
if (!ruleTestForm.title || !ruleTestForm.rulegroup) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ruleTestLoading.value = true
|
ruleTestLoading.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user