This commit is contained in:
InfinityPacer
2024-12-11 19:58:51 +08:00
13 changed files with 570 additions and 425 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.1.2", "version": "2.1.4",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {
@@ -32,6 +32,7 @@
"apexcharts-clevision": "^3.28.5", "apexcharts-clevision": "^3.28.5",
"axios": "1.6.8", "axios": "1.6.8",
"colorthief": "^2.4.0", "colorthief": "^2.4.0",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"express": "^4.18.2", "express": "^4.18.2",
"express-http-proxy": "^2.0.0", "express-http-proxy": "^2.0.0",
+2 -2
View File
@@ -115,7 +115,7 @@ function changeTheme(theme: string) {
// 保存主题到服务端 // 保存主题到服务端
try { try {
api.post('/user/config/Layout', { api.post('/user/config/Layout', {
theme: nextTheme theme: nextTheme,
}) })
} catch (e) { } catch (e) {
console.error('保存主题到服务端失败') console.error('保存主题到服务端失败')
@@ -176,7 +176,7 @@ async function saveCustomCSS() {
}, },
}) })
if (result.success) $toast.success('自定义CSS保存成功!') if (result.success) $toast.success('自定义CSS保存成功,请刷新页面生效')
} catch (e) { } catch (e) {
console.error('保存自定义 CSS 到服务端失败') console.error('保存自定义 CSS 到服务端失败')
} }
+5 -13
View File
@@ -1,3 +1,5 @@
import copy from 'copy-to-clipboard'
// 请求和获取剪贴板内容 // 请求和获取剪贴板内容
export async function getClipboardContent() { export async function getClipboardContent() {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
@@ -13,20 +15,10 @@ export async function getClipboardContent() {
} }
} }
// 将内容复制到剪贴板,兼容非安全域场景 // 将内容复制到剪贴板
export async function copyToClipboard(content: string) { export async function copyToClipboard(content: string) {
if (navigator.clipboard && window.isSecureContext) { const success = copy(content)
await navigator.clipboard.writeText(content) return success
} 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)
}
} }
// VAPID公钥转Uint8Array // VAPID公钥转Uint8Array
+4 -4
View File
@@ -98,8 +98,8 @@ function onClose() {
<DialogCloseBtn @click="onClose" /> <DialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start"> <div class="align-self-start">
<h5 class="text-h6 mb-1">{{ props.rule.id }}</h5> <h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
<div class="text-body-1 mb-3">{{ props.rule.name }}</div> <div class="text-body-1 mb-3">{{ props.rule.id }}</div>
</div> </div>
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" /> <VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText> </VCardText>
@@ -174,9 +174,9 @@ function onClose() {
<VCol cols="6"> <VCol cols="6">
<VTextField <VTextField
v-model="ruleInfo.publish_time" v-model="ruleInfo.publish_time"
placeholder="0" placeholder="0/1-10"
label="发布时间(分钟)" label="发布时间(分钟)"
hint="距离资源发布的最小时间间隔" hint="距离资源发布的最小时间间隔或时间区间"
persistent-hint persistent-hint
active active
/> />
+5 -3
View File
@@ -101,7 +101,7 @@ function filterCardClose(pri: string) {
} }
// 分享规则 // 分享规则
function shareRules() { async function shareRules() {
if (filterRuleCards.value.length === 0) return if (filterRuleCards.value.length === 0) return
const value = filterRuleCards.value const value = filterRuleCards.value
@@ -110,8 +110,10 @@ function shareRules() {
.join('>') .join('>')
try { try {
copyToClipboard(value) let success
$toast.success('优先级规则已复制到剪贴板') success = copyToClipboard(value)
if (await success) $toast.success('优先级规则已复制到剪贴板!')
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
} catch (error) { } catch (error) {
$toast.error('优先级规则复制失败!') $toast.error('优先级规则复制失败!')
console.error(error) console.error(error)
+24 -22
View File
@@ -149,28 +149,8 @@ const dropdownItems = ref([
</script> </script>
<template> <template>
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col"> <div>
<div class="me-n3 absolute bottom-0 right-3"> <VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col h-full">
<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 <div
class="relative flex flex-row items-start pa-3 justify-between grow" class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
@@ -213,6 +193,27 @@ const dropdownItems = ref([
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span> <span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</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> </VCardText>
</VCard> </VCard>
<!-- 安装插件进度框 --> <!-- 安装插件进度框 -->
@@ -225,4 +226,5 @@ const dropdownItems = ref([
<VersionHistory :history="props.plugin?.history" /> <VersionHistory :history="props.plugin?.history" />
</VCard> </VCard>
</VDialog> </VDialog>
</div>
</template> </template>
+39 -23
View File
@@ -390,30 +390,18 @@ watch(
</script> </script>
<template> <template>
<div>
<!-- 插件卡片 --> <!-- 插件卡片 -->
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col"> <VHover>
<div class="me-n3 absolute bottom-0 right-3"> <template #default="hover">
<IconBtn> <VCard
<VIcon icon="mdi-dots-vertical" /> v-if="isVisible"
<VMenu activator="parent" close-on-content-click> v-bind="hover.props"
<VList> :width="props.width"
<VListItem :height="props.height"
v-for="(item, i) in dropdownItems" @click="openPluginDetail"
v-show="item.show" class="flex flex-col h-full"
: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 <div
class="relative flex flex-row items-start pa-3 justify-between grow" class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
@@ -459,11 +447,38 @@ watch(
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span> <span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</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> </VCardText>
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5"> <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" /> <VIcon icon="mdi-new-box" class="text-white" />
</div> </div>
</VCard> </VCard>
</template>
</VHover>
<!-- 插件配置页面 --> <!-- 插件配置页面 -->
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value"> <VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
@@ -514,6 +529,7 @@ watch(
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
+7 -5
View File
@@ -271,12 +271,13 @@ function onSubscribeEditRemove() {
</script> </script>
<template> <template>
<div>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"
:key="props.media?.id" :key="props.media?.id"
class="flex flex-col rounded-lg" class="flex flex-col rounded-lg h-full"
:class="{ :class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded, 'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
@@ -316,10 +317,7 @@ function onSubscribeEditRemove() {
</template> </template>
<div class="absolute inset-0 subscribe-card-background"></div> <div class="absolute inset-0 subscribe-card-background"></div>
</VImg> </VImg>
<div <div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
v-if="subscribeState === 'P'"
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
></div>
</template> </template>
<div> <div>
<VCardText class="flex items-center"> <VCardText class="flex items-center">
@@ -371,6 +369,9 @@ function onSubscribeEditRemove() {
color="success" color="success"
/> />
</div> </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> </div>
</VCard> </VCard>
</template> </template>
@@ -399,6 +400,7 @@ function onSubscribeEditRemove() {
:sub="props.media" :sub="props.media"
@close="subscribeShareDialog = false" @close="subscribeShareDialog = false"
/> />
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
.subscribe-card-background { .subscribe-card-background {
+64 -8
View File
@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import api from '@/api' import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
@@ -28,6 +29,9 @@ const pluginId = ref(route.query.id)
// 当前排序字段 // 当前排序字段
const activeSort = ref(null) const activeSort = ref(null)
// 插件顺序配置
const orderConfig = ref<{ id: string }[]>([])
// 排序选项 // 排序选项
const sortOptions = [ const sortOptions = [
{ title: '热门', value: 'count' }, { title: '热门', value: 'count' },
@@ -104,6 +108,49 @@ const labelFilterOptions = ref<string[]>([])
// 插件库过滤项 // 插件库过滤项
const repoFilterOptions = 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))
}
}
// 排序
if (orderConfig.value) {
sortPluginOrder()
}
}
// 按order的顺序对插件进行排序
function sortPluginOrder() {
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 }))
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) { function initOptions(item: Plugin) {
const optionValue = (options: Array<string>, value: string | undefined) => { const optionValue = (options: Array<string>, value: string | undefined) => {
@@ -329,8 +376,9 @@ function handleRepoUrl(url: string | undefined) {
} }
// 加载时获取数据 // 加载时获取数据
onBeforeMount(async () => { onMounted(async () => {
await refreshData() await refreshData()
await loadPluginOrderConfig()
getPluginStatistics() getPluginStatistics()
if (activeTab.value != 'market' && pluginId.value) { if (activeTab.value != 'market' && pluginId.value) {
// 找到这个插件 // 找到这个插件
@@ -356,18 +404,26 @@ onBeforeMount(async () => {
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card"> <draggable
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`"> 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 <PluginCard
:count="PluginStatistics[data.id || '0']" :count="PluginStatistics[element.id || '0']"
:plugin="data" :plugin="element"
:action="pluginActions[data.id || '0']" :action="pluginActions[element.id || '0']"
@remove="refreshData" @remove="refreshData"
@save="refreshData" @save="refreshData"
@action-done="pluginActions[data.id || '0'] = false" @action-done="pluginActions[element.id || '0'] = false"
/> />
</template> </template>
</div> </draggable>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="dataList.length === 0 && isRefreshed"
error-code="404" error-code="404"
+62 -57
View File
@@ -8,41 +8,12 @@ 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' import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import internal from 'stream'
// 自定义规则列表 // 自定义规则列表
const customRules = ref<CustomRule[]>([]) const customRules = ref<CustomRule[]>([])
// 自定义规则类型,仅用于导入判断
const customRulesType = ref<CustomRule>({
// 规则ID
id: '',
// 名称
name: '',
// 包含
include: '',
// 排除
exclude: '',
// 大小范围
size_range: '',
// 最少做种人数
seeders: '',
// 发布时间
publish_time: '',
})
// 所有规则组列表 // 所有规则组列表
const filterRuleGroups = ref<FilterRuleGroup[]>([]) const filterRuleGroups = ref<FilterRuleGroup[]>([])
// 规则组类型,仅用于导入判断
const filterRuleGroupsType = ref<FilterRuleGroup>({
// 名称
name: '',
// 规则串
rule_string: '',
// 适用类媒体类型 None-全部 电影/电视剧
media_type: '',
// # 适用媒体类别 None-全部 对应二级分类
category: '',
})
// 种子优先规则 // 种子优先规则
const selectedTorrentPriority = ref<string>('seeder') const selectedTorrentPriority = ref<string>('seeder')
@@ -123,8 +94,6 @@ async function addCustomRule() {
customRules.value.push({ customRules.value.push({
id: id, id: id,
name: name, name: name,
include: '',
exclude: '',
}) })
} }
@@ -174,14 +143,11 @@ function addFilterRuleGroup() {
} }
filterRuleGroups.value.push({ filterRuleGroups.value.push({
name: name, 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 if (!rules || rules.length === 0) return
// 将卡片规则接装为字符串 // 将卡片规则接装为字符串
@@ -189,10 +155,13 @@ function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
// 复制到剪贴板 // 复制到剪贴板
try { try {
copyToClipboard(value) let success
$toast.success('优先级规则已复制到剪贴板') success = copyToClipboard(value)
} catch (error) { if (await success) $toast.success(`${type === 'custom' ? '自定义规则' : '优先级规则组'}已复制到剪贴板!`)
$toast.error('优先级规则复制失败!') 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 { try {
if (type === 'custom') { if (type === 'custom') {
for (const value of parsedCode) { if (!checkValueValidity(parsedCode, type)) return false
if (!validateValueAgainstInterface(value, customRulesType)) return false
}
const newCustomRules = extractCustomRules(parsedCode) || [] const newCustomRules = extractCustomRules(parsedCode) || []
customRules.value = [...customRules.value, ...newCustomRules] customRules.value = [...customRules.value, ...newCustomRules]
} else if (type === 'group') { } else if (type === 'group') {
for (const value of parsedCode) { if (!checkValueValidity(parsedCode, type)) return false
if (!validateValueAgainstInterface(value, filterRuleGroupsType)) return false
}
const newFilterRuleGroups = extractFilterRuleGroups(parsedCode) || [] const newFilterRuleGroups = extractFilterRuleGroups(parsedCode) || []
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups] filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups]
} else { } else {
@@ -246,6 +211,9 @@ function extractCustomRules(value: any) {
name: item.name, name: item.name,
include: item.include, include: item.include,
exclude: item.exclude, exclude: item.exclude,
size_range: item.size_rang,
seeders: item.seeders,
publish_time: item.publish_time,
} }
}) })
} catch (e) { } catch (e) {
@@ -269,14 +237,33 @@ function extractFilterRuleGroups(value: any) {
} }
} }
function validateValueAgainstInterface(value: any, interfaceDefinition: any): boolean { // 根据ID简单区分规则与规则组
console.log(value) function checkValueValidity(values: any, type: string): boolean {
console.log(interfaceDefinition)
try { try {
// 循环判断是否命中全部接口定义,暂时只检查是否存在,不检查是否多出 // 允许空值存在,不影响最终的导入
for (const key in interfaceDefinition.value) { if (!values) return true
if (value[key] === undefined) { if (!type) return false
$toast.error(`导入规则失败!输入了不符合要求的数据!`) 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 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) { function onRuleChange(rule: CustomRule, id: string) {
const index = customRules.value.findIndex(item => item.id === id) const index = customRules.value.findIndex(item => item.id === id)
@@ -383,12 +382,15 @@ onMounted(() => {
<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')"> <VBtn color="primary" variant="tonal" @click="importRules('custom')">
<VIcon icon="mdi-import" /> <VIcon icon="mdi-import" />
</VBtn> </VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(customRules)"> <VBtn color="info" variant="tonal" @click="shareRules(customRules, 'custom')">
<VIcon icon="mdi-share" /> <VIcon icon="mdi-share" />
</VBtn> </VBtn>
<VBtn color="error" variant="tonal" @click="deleteAllRules('custom')">
<VIcon icon="mdi-delete" />
</VBtn>
</VBtnGroup> </VBtnGroup>
</div> </div>
</VForm> </VForm>
@@ -431,12 +433,15 @@ onMounted(() => {
<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')"> <VBtn color="primary" variant="tonal" @click="importRules('group')">
<VIcon icon="mdi-import" /> <VIcon icon="mdi-import" />
</VBtn> </VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups)"> <VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups, 'group')">
<VIcon icon="mdi-share" /> <VIcon icon="mdi-share" />
</VBtn> </VBtn>
<VBtn color="error" variant="tonal" @click="deleteAllRules('group')">
<VIcon icon="mdi-delete" />
</VBtn>
</VBtnGroup> </VBtnGroup>
</div> </div>
</VForm> </VForm>
@@ -447,7 +452,7 @@ onMounted(() => {
<ImportCodeDialog <ImportCodeDialog
v-if="importCodeDialog" v-if="importCodeDialog"
v-model="importCodeDialog" v-model="importCodeDialog"
:title="`导入${importCodeType === 'custom' ? '自定义规则' : '规则组'}`" :title="`导入${importCodeType === 'custom' ? '自定义规则' : '优先级规则组'}`"
:dataType="importCodeType" :dataType="importCodeType"
@close="importCodeDialog = false" @close="importCodeDialog = false"
@save="saveCodeString" @save="saveCodeString"
+5 -3
View File
@@ -200,10 +200,12 @@ async function saveAdvancedSettings() {
} }
// 快捷复制到剪贴板 // 快捷复制到剪贴板
function copyValue(value: string) { async function copyValue(value: string) {
try { try {
copyToClipboard(value) let success
$toast.success('已复制到剪贴板') success = copyToClipboard(value)
if (await success) $toast.success('已复制到剪贴板!')
else $toast.error(`复制失败:可能是浏览器不支持或被用户阻止!`)
} catch (error) { } catch (error) {
$toast.error('复制失败!') $toast.error('复制失败!')
console.log(error) console.log(error)
+75 -15
View File
@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable'
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh' import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
import api from '@/api' import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
@@ -31,6 +32,52 @@ const subscribeEditDialog = ref(false)
// 历史记录弹窗 // 历史记录弹窗
const historyDialog = 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() { async function fetchData() {
try { try {
@@ -49,20 +96,28 @@ const loading = ref(false)
// 下拉刷新 // 下拉刷新
async function onRefresh({ done }: { done: any }) { async function onRefresh({ done }: { done: any }) {
await fetchData() await fetchData()
await loadSubscribeOrderConfig()
done('ok') done('ok')
} }
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅 // 显示的订阅列表
const filteredDataList = computed(() => { const displayList = ref<Subscribe[]>([])
// 监听dataList变化,同步更新displayList
watch(dataList, () => {
// 从Vuex Store中获取用户信息 // 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser const superUser = store.state.auth.superUser
const userName = store.state.auth.userName const userName = store.state.auth.userName
if (superUser) return dataList.value.filter(data => data.type === props.type) if (superUser) displayList.value = dataList.value.filter(data => data.type === props.type)
else return dataList.value.filter(data => data.type === props.type && data.username === userName) else displayList.value = dataList.value.filter(data => data.type === props.type && data.username === userName)
}) })
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => {})
onMounted(async () => { onMounted(async () => {
await fetchData() await fetchData()
await loadSubscribeOrderConfig()
if (props.subid) { if (props.subid) {
// 找到这个订阅 // 找到这个订阅
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString()) const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
@@ -75,7 +130,8 @@ onMounted(async () => {
onActivated(async () => { onActivated(async () => {
if (!loading.value) { if (!loading.value) {
fetchData() await fetchData()
await loadSubscribeOrderConfig()
} }
}) })
</script> </script>
@@ -83,17 +139,21 @@ onActivated(async () => {
<template> <template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VPullToRefresh v-model="loading" @load="onRefresh"> <VPullToRefresh v-model="loading" @load="onRefresh">
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1"> <draggable
<SubscribeCard v-if="displayList.length > 0"
v-for="data in filteredDataList" v-model="displayList"
:key="data.id" @end="saveSubscribeOrder"
:media="data" handle=".cursor-move"
@remove="fetchData" item-key="id"
@save="fetchData" tag="div"
/> :component-data="{ class: 'mx-3 grid gap-4 grid-subscribe-card p-1' }"
</div> >
<template #item="{ element }">
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
</template>
</draggable>
<NoDataFound <NoDataFound
v-if="filteredDataList.length === 0 && isRefreshed" v-if="displayList.length === 0 && isRefreshed"
error-code="404" error-code="404"
error-title="没有订阅" error-title="没有订阅"
error-description="请通过搜索添加电影电视剧订阅" error-description="请通过搜索添加电影电视剧订阅"
+7
View File
@@ -3101,6 +3101,13 @@ cookie@0.6.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== 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: core-js-compat@^3.31.0, core-js-compat@^3.36.1:
version "3.37.1" version "3.37.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee"