mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-01 05:40:41 +08:00
Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend
This commit is contained in:
23
src/@core/utils/image.ts
Normal file
23
src/@core/utils/image.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import ColorThief from 'colorthief'
|
||||
|
||||
// 将 RGB 转换为十六进制
|
||||
function rgbStringToHex(rgbArray: number[]): string {
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
|
||||
throw new Error('Invalid RGB string format')
|
||||
|
||||
const [r, g, b] = rgbArray
|
||||
|
||||
const toHex = (c: number): string => {
|
||||
const hex = c.toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
}
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(image: HTMLImageElement): Promise<string> {
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
}
|
||||
30
src/@core/utils/navigator.ts
Normal file
30
src/@core/utils/navigator.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 请求和获取剪贴板内容
|
||||
export async function getClipboardContent() {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return await navigator.clipboard.readText()
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('paste')
|
||||
const content = input.value
|
||||
document.body.removeChild(input)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// 将内容复制到剪贴板,兼容非安全域场景
|
||||
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)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
@@ -540,9 +540,6 @@ export interface Plugin {
|
||||
// 插件图标
|
||||
plugin_icon?: string
|
||||
|
||||
// 主题色
|
||||
plugin_color?: string
|
||||
|
||||
// 插件版本
|
||||
plugin_version?: string
|
||||
|
||||
|
||||
BIN
src/assets/images/logos/plugin.png
Normal file
BIN
src/assets/images/logos/plugin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -2,6 +2,8 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -13,6 +15,12 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['install'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -25,6 +33,17 @@ const progressText = ref('正在安装插件...')
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
async function installPlugin() {
|
||||
try {
|
||||
@@ -61,11 +80,54 @@ async function installPlugin() {
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath = computed(() => {
|
||||
return props.plugin?.plugin_icon?.startsWith('http')
|
||||
? props.plugin?.plugin_icon
|
||||
: `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 访问插件页面
|
||||
function visitPluginPage() {
|
||||
// 将raw.githubusercontent.com转换为项目地址
|
||||
let repoUrl = props.plugin?.repo_url
|
||||
if (repoUrl) {
|
||||
if (repoUrl.includes('raw.githubusercontent.com')) {
|
||||
if (!repoUrl.endsWith('/'))
|
||||
repoUrl += '/'
|
||||
|
||||
if (repoUrl.split('/').length < 6)
|
||||
repoUrl = `${repoUrl}main/`
|
||||
|
||||
try {
|
||||
const [user, repo] = repoUrl.split('/').slice(-4, -2)
|
||||
repoUrl = `https://github.com/${user}/${repo}`
|
||||
}
|
||||
catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
repoUrl = props.plugin?.author_url
|
||||
}
|
||||
window.open(repoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: visitPluginPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -76,11 +138,34 @@ const iconPath = computed(() => {
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
: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
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 right-5"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
@@ -91,11 +176,13 @@ const iconPath = computed(() => {
|
||||
size="8rem"
|
||||
>
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="isImageLoaded = true"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { Plugin } from '@/api/types'
|
||||
import FormRender from '@/components/render/FormRender.vue'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -17,6 +19,12 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -35,15 +43,26 @@ const pluginConfigForm = ref({})
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件详情页面
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 插件详情页面配置项
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 调用API卸载插件
|
||||
async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
@@ -94,7 +113,7 @@ async function loadPluginForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API读取详情页面
|
||||
// 调用API读取数据页面
|
||||
async function loadPluginPage() {
|
||||
try {
|
||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||
@@ -137,9 +156,9 @@ async function savePluginConf() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示插件详情
|
||||
// 显示插件数据
|
||||
async function showPluginInfo() {
|
||||
// 加载详情
|
||||
// 加载数据
|
||||
await loadPluginPage()
|
||||
pluginConfigDialog.value = false
|
||||
pluginInfoDialog.value = true
|
||||
@@ -157,10 +176,14 @@ async function showPluginConfig() {
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath = computed(() => {
|
||||
return props.plugin?.plugin_icon?.startsWith('http')
|
||||
? props.plugin?.plugin_icon
|
||||
: `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 重置插件
|
||||
@@ -197,10 +220,15 @@ async function resetPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
title: '查看数据',
|
||||
value: 1,
|
||||
show: props.plugin?.has_page,
|
||||
props: {
|
||||
@@ -209,7 +237,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '配置',
|
||||
title: '设置',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
@@ -237,6 +265,15 @@ const dropdownItems = ref([
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
click: visitAuthorPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -255,7 +292,7 @@ const dropdownItems = ref([
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
@@ -286,10 +323,13 @@ const dropdownItems = ref([
|
||||
size="8rem"
|
||||
>
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
@@ -324,7 +364,7 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||
查看详情
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@@ -337,7 +377,7 @@ const dropdownItems = ref([
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件详情页面 -->
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
scrollable
|
||||
|
||||
@@ -25,7 +25,6 @@ const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
console.log(item)
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
39
src/components/form/ImportCodeForm.vue
Normal file
39
src/components/form/ImportCodeForm.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -199,7 +199,7 @@ async function updateSiteInfo() {
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
v-model="siteForm.limit_count"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
|
||||
@@ -39,6 +39,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
save_path: ''
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -321,6 +322,17 @@ watchEffect(() => {
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.save_path"
|
||||
label="保存路径"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
|
||||
@@ -40,6 +40,9 @@ const dataList = ref<TransferHistory[]>([])
|
||||
// 搜索
|
||||
const search = ref('')
|
||||
|
||||
// 搜索提示词列表
|
||||
const searchHintList = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -89,6 +92,7 @@ async function fetchData({
|
||||
|
||||
dataList.value = result.data.list
|
||||
totalItems.value = result.data.total
|
||||
searchHintList.value = [...new Set(dataList.value.map(item => item.title || ''))].filter(title => title !== '')
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -294,9 +298,10 @@ const dropdownItems = ref([
|
||||
<VRow>
|
||||
<VCol> 历史记录 </VCol>
|
||||
<VCol>
|
||||
<VTextField
|
||||
<VCombobox
|
||||
key="search_navbar"
|
||||
v-model="search"
|
||||
:items="searchHintList"
|
||||
class="text-disabled"
|
||||
density="compact"
|
||||
label="搜索"
|
||||
@@ -306,6 +311,7 @@ const dropdownItems = ref([
|
||||
hide-details
|
||||
flat
|
||||
rounded
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -30,6 +32,12 @@ const defaultFilterRules = ref({
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
@@ -227,6 +235,49 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules() {
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
@@ -264,6 +315,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importCodeDialog = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -332,6 +413,17 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -27,12 +29,21 @@ const allSites = ref<Site[]>([])
|
||||
// 选中订阅站点
|
||||
const selectedRssSites = ref<number[]>([])
|
||||
|
||||
// 当前规则类型
|
||||
const currentRuleType = ref('SubscribeFilterRules')
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
async function querySelectedRssSites() {
|
||||
try {
|
||||
@@ -244,6 +255,66 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules(ruleType: string) {
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (ruleType === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
async function importRules(ruleType: string) {
|
||||
currentRuleType.value = ruleType
|
||||
importCodeString.value = ''
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
if (!currentRuleType.value)
|
||||
return
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (currentRuleType.value === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
@@ -282,6 +353,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -318,6 +419,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="洗版优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -386,6 +517,17 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -173,7 +173,7 @@ onMounted(() => {
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
title="支持的配置格式:"
|
||||
title="支持的配置格式(注意空格):"
|
||||
>
|
||||
<span
|
||||
v-html="`
|
||||
|
||||
Reference in New Issue
Block a user