Compare commits

...

36 Commits

Author SHA1 Message Date
jxxghp
8b53cd0a09 feat: 为多个组件的 VMenu 添加 scrim 属性 2025-04-13 09:42:58 +08:00
jxxghp
3d7a0d9b0d feat: 为多个组件添加边框样式 2025-04-13 09:19:20 +08:00
jxxghp
114844ad48 refactor: 移除 UserNotification 组件中多余的样式定义,简化代码 2025-04-12 09:59:41 +08:00
jxxghp
f83f709080 feat: 调整 SubscribeCard 组件的图像宽度,优化搜索框对齐和样式,移除多余的样式定义 2025-04-12 09:45:43 +08:00
jxxghp
999a6e7c6e fix 站点列表权限问题 2025-04-12 09:30:17 +08:00
jxxghp
2fd4c0b2ea feat: 优化搜索框样式,调整图标和文本,提升用户体验 2025-04-11 15:45:02 +08:00
jxxghp
16d62642f6 fix:大量tooltip对象 2025-04-11 15:20:36 +08:00
jxxghp
96a0ce8c5f feat: 为排序选择器添加 variant 属性,简化样式并移除多余 CSS 2025-04-11 08:36:24 +08:00
jxxghp
7703d8157c feat: 移除多个组件中的多余 variant 属性,简化代码 2025-04-11 08:29:00 +08:00
jxxghp
87aa4e902c feat: 优化 SlideView 组件样式,调整圆角和阴影效果 2025-04-11 07:09:50 +08:00
jxxghp
c86f32fab5 更新 styles.scss 2025-04-10 23:13:25 +08:00
jxxghp
f85ac34753 feat: 增强工作流侧边栏,支持移动端显示和组件点击事件处理 2025-04-10 21:12:48 +08:00
jxxghp
f58d4fcb7e 更新 recommend.vue 2025-04-09 23:01:13 +08:00
jxxghp
675c32cee3 feat: 使用 VCard 和 VProgressLinear 组件优化资源页面的加载进度显示 2025-04-09 22:39:41 +08:00
jxxghp
de011b35db 更新 TorrentCardListView.vue 2025-04-09 22:04:25 +08:00
jxxghp
288a7ebc20 更新 TorrentRowListView.vue 2025-04-09 22:03:47 +08:00
jxxghp
d7c3167ecd feat: 优化 TorrentRowListView 组件的数据过滤和排序逻辑 2025-04-09 21:49:15 +08:00
jxxghp
3205ae3ebe feat: 移除多个组件的背景颜色以优化样式 2025-04-09 21:31:57 +08:00
jxxghp
2ba609fb78 更新 package.json 2025-04-09 19:48:08 +08:00
jxxghp
7e70b1b7ab fix: 调整组件的内边距和边距以优化布局 2025-04-09 15:59:00 +08:00
jxxghp
561bdf4137 feat: 优化 ScrollToTopBtn 组件样式和布局,调整按钮位置和尺寸 2025-04-09 15:22:55 +08:00
jxxghp
22a2bb65c8 feat: 优化 TransferHistoryView 组件的布局和样式 2025-04-09 15:05:31 +08:00
jxxghp
c4f6db9f9f feat: 优化 ForkSubscribeDialog 组件按钮 2025-04-09 13:27:26 +08:00
jxxghp
e5d2140ea3 fix: 移除推荐内容的内边距以优化布局 2025-04-09 11:55:51 +08:00
jxxghp
83e57deec3 fix: 调整布局组件的宽度和内边距以改善响应式设计 2025-04-09 11:52:49 +08:00
jxxghp
fc357a03e5 feat: 更新 HeaderTab 组件以支持动态项和排序功能 2025-04-09 10:39:17 +08:00
jxxghp
f031077fbd feat: 更新插件市场设置对话框和订阅页面 2025-04-09 08:29:33 +08:00
jxxghp
02de63210d feat: 添加 HeaderTab 组件 2025-04-09 08:07:53 +08:00
jxxghp
98610e3e0d feat: 隐藏小屏幕设备上的回到顶部按钮 2025-04-08 21:57:35 +08:00
jxxghp
bb6cfd9d0e feat: 回到顶部按钮组件化 2025-04-08 21:52:04 +08:00
jxxghp
57c6d7e8f3 Merge pull request #322 from madrays/v2
继续优化探索页UI
2025-04-08 21:40:11 +08:00
jxxghp
6d8b850b15 Merge branch 'v2' into v2 2025-04-08 21:39:48 +08:00
madrays
db6c3ea36c 继续优化探索页UI 2025-04-08 20:43:43 +08:00
jxxghp
0ddf7ab070 feat: 优化主题切换器和用户通知组件的样式 2025-04-08 19:34:27 +08:00
jxxghp
93686bd354 feat: 添加推荐源类型字段,优化推荐页面分类逻辑和样式 2025-04-08 19:14:16 +08:00
jxxghp
89e4a68a03 feat: add ScrollToTopBtn component and integrate it into multiple pages
- Added ScrollToTopBtn component for smooth scrolling to the top of the page.
- Registered ScrollToTopBtn in main.ts.
- Integrated ScrollToTopBtn into browse.vue, discover.vue, recommend.vue, resource.vue pages.
- Updated components.d.ts to include ScrollToTopBtn type definition.
- Refactored MediaCard.vue and SlideView.vue for improved hover effects and styling.
- Cleaned up unused styles and optimized existing styles for better performance and readability.
2025-04-08 17:43:20 +08:00
54 changed files with 1815 additions and 1462 deletions

1
components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default']
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
}

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.3.8",
"version": "2.3.9",
"private": true,
"bin": "dist/service.js",
"scripts": {

View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
// 控制回到顶部按钮的可见性
const showScrollToTop = ref(false)
const scrollThreshold = 200 // 滚动多少像素后显示按钮
// 滚动事件处理函数
const handleScroll = () => {
showScrollToTop.value = window.scrollY > scrollThreshold
}
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(async () => {
// Add scroll event listener
window.addEventListener('scroll', handleScroll)
// Initial check for scroll-to-top
handleScroll()
})
onUnmounted(() => {
// Remove scroll event listener
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<div class="global-action-buttons d-none d-sm-block">
<Transition name="scroll-fade">
<button v-show="showScrollToTop" class="global-action-button" @click="scrollToTop">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 14L12 9L17 14"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</Transition>
</div>
</template>
<style lang="scss" scoped>
/* Global Action Button Styles (FAB) */
.global-action-buttons {
position: fixed;
z-index: 100;
display: flex;
flex-direction: column;
gap: 16px;
inset-block-end: 30px;
inset-inline-end: 30px;
}
.global-action-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
border-radius: 50%;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.8);
block-size: 44px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 8%);
color: rgb(var(--v-theme-on-surface));
cursor: pointer;
inline-size: 44px;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
&:hover {
background-color: rgba(var(--v-theme-background), 0.95);
color: rgb(var(--v-theme-primary));
transform: translateY(-4px);
}
svg {
block-size: 20px;
inline-size: 20px;
transition: all 0.3s ease;
}
}
</style>

View File

@@ -189,13 +189,13 @@ onMounted(() => {
</script>
<template>
<VMenu v-if="props.themes" class="theme-menu">
<VMenu v-if="props.themes" class="theme-menu" scrim>
<template v-slot:activator="{ props }">
<IconBtn v-bind="props">
<VIcon :icon="getThemeIcon" />
</IconBtn>
</template>
<VList elevation="0" class="theme-switcher-list">
<VList class="theme-switcher-list pt-0">
<VCardItem class="theme-switcher-header">
<VCardTitle class="font-weight-medium text-primary">主题选择</VCardTitle>
</VCardItem>
@@ -234,8 +234,14 @@ onMounted(() => {
</VMenu>
<!-- 自定义 CSS -- -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard title="自定义主题风格">
<DialogCloseBtn @click="cssDialog = false" />
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="me-2" />
自定义主题风格
</VCardTitle>
<DialogCloseBtn @click="cssDialog = false" />
</VCardItem>
<VDivider />
<VAceEditor
v-model:value="customCSS"

View File

@@ -116,7 +116,7 @@ export default defineComponent({
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;

View File

@@ -178,7 +178,7 @@ export default defineComponent({
}
&:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: calc(variables.$layout-vertical-nav-width + 8px);
padding-inline-start: calc(variables.$layout-vertical-nav-width + 0.5rem);
}
// Adjust right column pl when vertical nav is collapsed
@@ -210,8 +210,8 @@ export default defineComponent({
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
.layout-navbar {
inline-size: calc(100% - 0px);
margin-inline-start: 8px;
inline-size: calc(100% - 0.5rem);
margin-inline-start: 0.5rem;
padding-inline-start: 0;
}
}

View File

@@ -51,7 +51,7 @@ body,
& > div:first-child {
position: relative;
flex: auto;
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
}
}
}

View File

@@ -1265,6 +1265,8 @@ export interface RecommendSource {
name: string
// 媒体数据源API地址
api_path: string
// 类型
type: string
}
// 站点资源分类

View File

@@ -431,7 +431,7 @@ function onRemoveSubscribe() {
:width="props.width"
class="outline-none shadow ring-gray-500 media-card"
:class="{
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goMediaDetail(hover.isHovering ?? false)"
@@ -450,7 +450,7 @@ function onRemoveSubscribe() {
</div>
</template>
</VImg>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError || searchMenuShow"
@@ -533,15 +533,3 @@ function onRemoveSubscribe() {
@close="chooseSiteDialog = false"
/>
</template>
<style lang="scss" scoped>
.media-card {
position: relative;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.03);
z-index: 2;
}
}
</style>

View File

@@ -208,13 +208,7 @@ const dropdownItems = ref([
<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"
>
<VListItem v-for="(item, i) in dropdownItems" v-show="item.show" :key="i" @click="item.props.click">
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>

View File

@@ -393,7 +393,6 @@ watch(
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>

View File

@@ -228,41 +228,18 @@ onMounted(() => {
<!-- 站点特性图标 -->
<div class="site-features flex items-center gap-1 ml-auto">
<VTooltip>
<template #activator="{ props }">
<div v-if="cardProps.site?.limit_interval" v-bind="props" class="feature-icon-wrapper">
<VIcon icon="mdi-speedometer" size="16" class="site-feature-icon" />
</div>
</template>
<span>流控</span>
</VTooltip>
<VTooltip>
<template #activator="{ props }">
<div v-if="cardProps.site?.proxy === 1" v-bind="props" class="feature-icon-wrapper">
<VIcon icon="mdi-network-outline" size="16" class="site-feature-icon" />
</div>
</template>
<span>代理</span>
</VTooltip>
<VTooltip>
<template #activator="{ props }">
<div v-if="cardProps.site?.render === 1" v-bind="props" class="feature-icon-wrapper">
<VIcon icon="mdi-apple-safari" size="16" class="site-feature-icon" />
</div>
</template>
<span>仿真</span>
</VTooltip>
<VTooltip>
<template #activator="{ props }">
<div v-if="cardProps.site?.filter" v-bind="props" class="feature-icon-wrapper">
<VIcon icon="mdi-filter-cog-outline" size="16" class="site-feature-icon" />
</div>
</template>
<span>过滤</span>
</VTooltip>
<div v-if="cardProps.site?.limit_interval" class="feature-icon-wrapper">
<VIcon icon="mdi-speedometer" size="16" class="site-feature-icon" />
</div>
<div v-if="cardProps.site?.proxy === 1" class="feature-icon-wrapper">
<VIcon icon="mdi-network-outline" size="16" class="site-feature-icon" />
</div>
<div v-if="cardProps.site?.render === 1" class="feature-icon-wrapper">
<VIcon icon="mdi-apple-safari" size="16" class="site-feature-icon" />
</div>
<div v-if="cardProps.site?.filter" class="feature-icon-wrapper">
<VIcon icon="mdi-filter-cog-outline" size="16" class="site-feature-icon" />
</div>
</div>
</div>
</div>
@@ -309,69 +286,48 @@ onMounted(() => {
<!-- 右侧操作按钮区 -->
<div class="site-card-actions">
<VTooltip>
<template #activator="{ props }">
<IconBtn
v-bind="props"
elevation="0"
class="site-action-btn test-btn"
@click.stop="testSite"
:class="{ 'testing': testButtonDisable }"
>
<div class="test-btn-content">
<div class="pulse-dot" :class="statColor"></div>
</div>
<div v-if="testButtonDisable" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner-circle"></div>
<div class="spinner-circle-dot"></div>
</div>
<span class="loading-text">测试中</span>
</div>
</IconBtn>
</template>
<span>测试站点连通性</span>
</VTooltip>
<VTooltip v-if="!cardProps.site?.public">
<template #activator="{ props }">
<IconBtn v-bind="props" elevation="0" class="site-action-btn" @click.stop="handleSiteUserData">
<VIcon icon="mdi-chart-bell-curve" size="18" />
</IconBtn>
</template>
<span>查看站点数据</span>
</VTooltip>
<VTooltip v-if="!cardProps.site?.public">
<template #activator="{ props }">
<IconBtn v-bind="props" elevation="0" class="site-action-btn" @click.stop="handleSiteUpdate">
<VIcon icon="mdi-refresh" size="18" />
</IconBtn>
</template>
<span>更新Cookie/UA</span>
</VTooltip>
<VTooltip>
<template #activator="{ props }">
<IconBtn v-bind="props" elevation="0" class="site-action-btn more-btn">
<VIcon icon="mdi-dots-vertical" size="18" />
<VMenu activator="parent" close-on-content-click location="left">
<VList density="compact" nav class="dropdown-menu">
<VListItem variant="plain" @click="siteEditDialog = true" base-color="info">
<template #prepend>
<VIcon icon="mdi-file-edit-outline" size="small" />
</template>
<VListItemTitle>编辑站点</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="deleteSiteInfo">
<template #prepend>
<VIcon icon="mdi-delete-outline" size="small" color="error" />
</template>
<VListItemTitle class="text-error">删除站点</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
<span>更多操作</span>
</VTooltip>
<IconBtn
elevation="0"
class="site-action-btn test-btn"
@click.stop="testSite"
:class="{ 'testing': testButtonDisable }"
>
<div class="test-btn-content">
<div class="pulse-dot" :class="statColor"></div>
</div>
<div v-if="testButtonDisable" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner-circle"></div>
<div class="spinner-circle-dot"></div>
</div>
<span class="loading-text">测试中</span>
</div>
</IconBtn>
<IconBtn elevation="0" class="site-action-btn" @click.stop="handleSiteUserData">
<VIcon icon="mdi-chart-bell-curve" size="18" />
</IconBtn>
<IconBtn elevation="0" class="site-action-btn" @click.stop="handleSiteUpdate">
<VIcon icon="mdi-refresh" size="18" />
</IconBtn>
<IconBtn elevation="0" class="site-action-btn more-btn">
<VIcon icon="mdi-dots-vertical" size="18" />
<VMenu activator="parent" close-on-content-click location="left">
<VList density="compact" nav class="dropdown-menu">
<VListItem @click="siteEditDialog = true" base-color="info">
<template #prepend>
<VIcon icon="mdi-file-edit-outline" size="small" />
</template>
<VListItemTitle>编辑站点</VListItemTitle>
</VListItem>
<VListItem @click="deleteSiteInfo">
<template #prepend>
<VIcon icon="mdi-delete-outline" size="small" color="error" />
</template>
<VListItemTitle class="text-error">删除站点</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</VCard>

View File

@@ -311,12 +311,7 @@ function onSubscribeEditRemove() {
<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"
>
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
@@ -340,7 +335,7 @@ function onSubscribeEditRemove() {
</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">
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
<template #placeholder>
<div class="w-full h-full">

View File

@@ -194,7 +194,6 @@ onMounted(() => {
.torrent-item {
padding: 12px;
background-color: rgb(var(--v-theme-surface));
box-shadow: none;
margin-block-end: 8px;
transition: background-color 0.2s ease, transform 0.2s ease;

View File

@@ -216,7 +216,6 @@ onUnmounted(() => {
class="action-btn"
>
<VIcon icon="mdi-pencil" />
<VTooltip v-if="!isMobile" activator="parent" location="bottom">编辑用户</VTooltip>
</VBtn>
<VBtn
@@ -229,7 +228,6 @@ onUnmounted(() => {
class="action-btn"
>
<VIcon icon="mdi-delete" />
<VTooltip v-if="!isMobile" activator="parent" location="bottom">删除用户</VTooltip>
</VBtn>
</div>
</div>
@@ -282,7 +280,6 @@ onUnmounted(() => {
.user-card {
position: relative;
overflow: hidden;
background: rgb(var(--v-theme-surface));
transition: all 0.3s ease;
}
@@ -341,7 +338,6 @@ onUnmounted(() => {
justify-content: center;
border: 1px solid rgba(var(--v-theme-warning), 0.5);
border-radius: 50%;
background: rgb(var(--v-theme-surface));
block-size: 18px;
inline-size: 18px;
margin-block: 0;

View File

@@ -206,47 +206,37 @@ const resolveProgress = (item: Workflow) => {
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" base-color="primary" @click="handleEdit(workflow)">
<VListItem base-color="primary" @click="handleEdit(workflow)">
<template #prepend>
<VIcon icon="mdi-note-edit" />
</template>
<VListItemTitle>编辑任务</VListItemTitle>
</VListItem>
<VListItem
v-if="workflow.current_action"
variant="plain"
base-color="info"
@click="handleRun(workflow, false)"
>
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, false)">
<template #prepend>
<VIcon icon="mdi-play-speed" />
</template>
<VListItemTitle>继续执行</VListItemTitle>
</VListItem>
<VListItem
v-if="workflow.current_action"
variant="plain"
base-color="info"
@click="handleRun(workflow, true)"
>
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, true)">
<template #prepend>
<VIcon icon="mdi-replay" />
</template>
<VListItemTitle>重新执行</VListItemTitle>
</VListItem>
<VListItem v-else variant="plain" base-color="info" @click="handleRun(workflow, true)">
<VListItem v-else base-color="info" @click="handleRun(workflow, true)">
<template #prepend>
<VIcon icon="mdi-run" />
</template>
<VListItemTitle>立即执行</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="warning" @click="handleReset(workflow)">
<VListItem base-color="warning" @click="handleReset(workflow)">
<template #prepend>
<VIcon icon="mdi-restore-alert" />
</template>
<VListItemTitle>重置任务</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="error" @click="handleDelete(workflow)">
<VListItem base-color="error" @click="handleDelete(workflow)">
<template #prepend>
<VIcon icon="mdi-delete" />
</template>

View File

@@ -230,26 +230,16 @@ onMounted(() => {
@click="doFork"
prepend-icon="mdi-heart"
:loading="processing"
class="mb-2 me-2"
>
订阅
</VBtn>
<VBtn
v-if="props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID"
color="error"
:disabled="deleting"
@click="doDelete"
prepend-icon="mdi-delete"
:loading="deleting"
class="ms-2"
>
取消分享
</VBtn>
<VBtn
v-else-if="isFollowed && props.media?.share_uid"
v-if="isFollowed && props.media?.share_uid"
color="warning"
@click="unfollowUser"
prepend-icon="mdi-account-remove"
class="ms-2"
class="mb-2 me-2"
>
取消关注
</VBtn>
@@ -258,10 +248,24 @@ onMounted(() => {
@click="followUser"
color="info"
prepend-icon="mdi-account-plus"
class="ms-2"
class="mb-2 me-2"
>
关注
</VBtn>
<VBtn
v-if="
(props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID) ||
globalSettings.SUBSCRIBE_SHARE_MANAGE
"
color="error"
:disabled="deleting"
@click="doDelete"
prepend-icon="mdi-delete"
:loading="deleting"
class="mb-2 me-2"
>
取消分享
</VBtn>
</div>
<div class="text-xs mt-2" v-if="props.media?.count">
<VIcon icon="mdi-fire" /> {{ props.media?.count?.toLocaleString() }} 次复用

View File

@@ -2,11 +2,6 @@
import api from '@/api'
import { useToast } from 'vue-toast-notification'
// 输入参数
const props = defineProps({
title: String,
})
const $toast = useToast()
// 插件仓库设置字符串
@@ -47,8 +42,14 @@ onMounted(() => {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard title="插件仓库设置" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCard class="rounded-t">
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-store-cog" class="me-2" />
插件仓库设置
</VCardTitle>
<DialogCloseBtn @click="emit('close')" />
</VCardItem>
<VCardText class="pt-2">
<VTextarea
v-model="repoString"

View File

@@ -152,66 +152,32 @@ async function fetchSubscribes() {
}
}
// 保存用户站点选择到本地
const saveUserSitePreferences = () => {
try {
localStorage.setItem('MP_SelectedSites', JSON.stringify(selectedSites.value))
} catch (err) {
console.error('保存站点选择失败:', err)
}
}
// 从本地或接口加载用户站点偏好设置
// 从接口加载用户站点偏好设置
const loadUserSitePreferences = async () => {
try {
// 先尝试从本地存储获取
const storedSites = localStorage.getItem('MP_SelectedSites')
if (storedSites) {
selectedSites.value = JSON.parse(storedSites)
console.log('从本地加载站点选择:', selectedSites.value)
return
}
// 如果本地没有,尝试从接口获取系统预设
const result = await api.get('system/setting/IndexerSites')
if (result && result.data && result.data.value) {
selectedSites.value = result.data.value
console.log('从系统预设加载站点选择:', selectedSites.value)
return
}
} catch (err) {
console.error('加载站点选择失败:', err)
console.error(err)
}
}
// 获取站点分类信息
const getSiteCategories = () => {
api
.get('site/')
.then(async (res: any) => {
if (res && Array.isArray(res)) {
allSites.value = res.filter((site: any) => site.is_active) || []
// 加载用户站点选择
await loadUserSitePreferences()
// 如果没有选择任何站点并且有可用站点,才默认选择全部
if (selectedSites.value.length === 0 && allSites.value.length > 0) {
selectedSites.value = allSites.value.map((site: Site) => site.id)
}
} else if (res.data && Array.isArray(res.data)) {
allSites.value = res.data.filter((site: any) => site.is_active) || []
// 加载用户站点选择
await loadUserSitePreferences()
// 如果没有选择任何站点并且有可用站点,才默认选择全部
if (selectedSites.value.length === 0 && allSites.value.length > 0) {
selectedSites.value = allSites.value.map((site: Site) => site.id)
}
}
console.log('站点数据:', allSites.value)
console.log('已选站点:', selectedSites.value)
})
.catch(err => {
console.error('获取站点数据失败:', err)
})
// 查询所有站点
async function queryAllSites() {
try {
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
// 如果没有选择任何站点并且有可用站点,才默认选择全部
if (selectedSites.value.length === 0 && allSites.value.length > 0) {
selectedSites.value = allSites.value.map((site: Site) => site.id)
}
} catch (error) {
console.log(error)
}
}
// 打开站点选择对话框
@@ -235,19 +201,11 @@ function searchSites(sites: number[]) {
searchTorrent()
}
// 选择站点
function chooseSitesDone(sites: number[]) {
chooseSiteDialog.value = false
selectedSites.value = sites
}
// 搜索资源
function searchTorrent() {
if (!searchWord.value) return
// 记录搜索词
saveRecentSearches(searchWord.value)
// 保存用户站点选择
saveUserSitePreferences()
// 跳转到搜索页面
router.push({
path: '/resource',
@@ -335,7 +293,8 @@ onMounted(() => {
fetchInstalledPlugins()
fetchSubscribes()
loadRecentSearches()
getSiteCategories()
loadUserSitePreferences()
if (superUser) queryAllSites()
})
</script>
<template>
@@ -343,6 +302,9 @@ onMounted(() => {
<VCard class="search-dialog">
<!-- 搜索输入框 -->
<VCardItem class="pa-4 pa-sm-5 search-box-container">
<template #prepend>
<VIcon icon="mdi-magnify" color="primary" size="x-large" />
</template>
<VCombobox
ref="searchWordInput"
v-model="searchWord"
@@ -353,28 +315,22 @@ onMounted(() => {
@keydown.enter="searchMedia('media')"
hide-details
clearable
>
<template #prepend>
<VIcon icon="mdi-magnify" color="primary" class="search-icon" />
</template>
</VCombobox>
<DialogCloseBtn inner-class="close-btn" @click="emit('close')">
<template #default>
<VIcon icon="mdi-close-circle" color="error" />
</template>
</DialogCloseBtn>
/>
<template #append>
<IconBtn>
<VIcon icon="mdi-close" color="primary" @click="emit('close')" size="x-large" />
</IconBtn>
</template>
</VCardItem>
<VDivider class="search-divider" />
<VDivider />
<!-- 主搜索结果区域 -->
<VCardText class="search-results-container pa-0">
<!-- 有搜索词时显示结果 -->
<VList lines="two" v-if="searchWord" class="search-list py-2">
<!-- 搜索结果分组标题 -->
<VListSubheader class="primary-text font-weight-medium text-uppercase py-2 px-4 px-sm-6">
<span class="category-title">媒体搜索</span>
</VListSubheader>
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 媒体 </VListSubheader>
<!-- 媒体搜索选项 -->
<VHover>
@@ -396,7 +352,7 @@ onMounted(() => {
/>
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium"> 电影电视剧 </VListItemTitle>
<VListItemTitle class="font-weight-medium"> 电影电视剧 </VListItemTitle>
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的影视作品
</VListItemSubtitle>
@@ -426,7 +382,7 @@ onMounted(() => {
/>
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium"> 系列合集 </VListItemTitle>
<VListItemTitle class="font-weight-medium"> 系列合集 </VListItemTitle>
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的系列作品
</VListItemSubtitle>
@@ -456,7 +412,7 @@ onMounted(() => {
/>
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium"> 演职人员 </VListItemTitle>
<VListItemTitle class="font-weight-medium"> 演职人员 </VListItemTitle>
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的演员导演等
</VListItemSubtitle>
@@ -482,7 +438,7 @@ onMounted(() => {
<VIcon icon="mdi-history" :color="hover.isHovering ? 'primary' : 'medium-emphasis'" size="small" />
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium"> 整理记录 </VListItemTitle>
<VListItemTitle class="font-weight-medium"> 整理记录 </VListItemTitle>
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的历史记录
</VListItemSubtitle>
@@ -496,9 +452,7 @@ onMounted(() => {
<!-- 其他搜索结果 -->
<template v-if="matchedSubscribeItems.length > 0">
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
<VListSubheader class="primary-text font-weight-medium text-uppercase py-2 px-4 px-sm-6">
<span class="category-title">订阅内容</span>
</VListSubheader>
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 订阅 </VListSubheader>
<VHover v-for="subscribe in matchedSubscribeItems" :key="subscribe.id">
<template #default="hover">
@@ -519,7 +473,7 @@ onMounted(() => {
/>
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium">
<VListItemTitle class="font-weight-medium">
{{ subscribe.name
}}<span v-if="subscribe.season" class="text-body-2"> {{ subscribe.season }} </span>
</VListItemTitle>
@@ -536,9 +490,7 @@ onMounted(() => {
<template v-if="matchedMenuItems.length > 0">
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
<VListSubheader class="primary-text font-weight-medium text-uppercase py-2 px-4 px-sm-6">
<span class="category-title">功能菜单</span>
</VListSubheader>
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 功能 </VListSubheader>
<VHover v-for="menu in matchedMenuItems" :key="menu.title">
<template #default="hover">
@@ -559,7 +511,7 @@ onMounted(() => {
/>
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium">
<VListItemTitle class="font-weight-medium">
{{ menu.title }}
</VListItemTitle>
<VListItemSubtitle v-if="menu.description" class="text-body-2 text-medium-emphasis mt-1">
@@ -575,9 +527,7 @@ onMounted(() => {
<template v-if="matchedPluginItems.length > 0">
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
<VListSubheader class="primary-text font-weight-medium text-uppercase py-2 px-4 px-sm-6">
<span class="category-title">插件</span>
</VListSubheader>
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 插件 </VListSubheader>
<VHover v-for="plugin in matchedPluginItems" :key="plugin.id">
<template #default="hover">
@@ -594,7 +544,7 @@ onMounted(() => {
<VIcon icon="mdi-puzzle" :color="hover.isHovering ? 'primary' : 'medium-emphasis'" size="small" />
</div>
</template>
<VListItemTitle class="text-subtitle-1 font-weight-medium">
<VListItemTitle class="font-weight-medium">
{{ plugin.plugin_name }}
</VListItemTitle>
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
@@ -611,14 +561,12 @@ onMounted(() => {
<!-- 将站点资源搜索移到最底部 -->
<template v-if="searchWord">
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
<VListSubheader class="primary-text font-weight-medium text-uppercase py-2 px-4 px-sm-6">
<span class="category-title">站点资源搜索</span>
</VListSubheader>
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 站点资源 </VListSubheader>
<VCard class="mx-3 mx-sm-6 mb-4 mt-2 site-search-card">
<VCardText class="pa-3 pa-sm-4">
<div class="d-flex flex-column">
<div class="d-flex align-center mb-3">
<div class="d-flex align-center">
<div class="search-icon-wrapper mr-3">
<VIcon icon="mdi-file-search" color="primary" size="small" />
</div>
@@ -634,14 +582,16 @@ onMounted(() => {
prepend-icon="mdi-magnify"
size="small"
variant="flat"
rounded="pill"
class="search-btn"
>
搜索
</VBtn>
</div>
<div class="d-flex align-center flex-wrap site-chips-container mt-1 py-2 px-2 px-sm-3">
<div
v-if="superUser"
class="d-flex align-center flex-wrap site-chips-container mt-4 py-2 px-2 px-sm-3"
>
<div class="d-flex align-center flex-wrap flex-grow-1">
<VChip
v-if="selectedSites.length > 0"
@@ -676,6 +626,7 @@ onMounted(() => {
color="primary"
@click="openSiteDialog"
class="ml-auto site-select-btn"
rounded="pill"
>
选择站点
<VIcon size="small" class="ml-1">mdi-cog-outline</VIcon>
@@ -725,47 +676,40 @@ onMounted(() => {
v-model="chooseSiteDialog"
:sites="allSites"
:selected="selectedSites"
:savebtn="true"
@search="searchSites"
@close="chooseSiteDialog = false"
@reload="getSiteCategories"
@save="chooseSitesDone"
@reload="queryAllSites"
/>
</template>
<style scoped>
.search-dialog {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 8%);
}
.site-dialog {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 8%);
}
.search-divider {
opacity: 0.08;
}
.search-box-container {
position: relative;
background-color: rgb(var(--v-theme-background));
}
.close-btn {
position: absolute;
right: 1.2rem;
top: 1.4rem;
background-color: rgba(var(--v-theme-on-surface), 0.04);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(var(--v-theme-on-surface), 0.04);
block-size: 36px;
inline-size: 36px;
inset-block-start: 1.4rem;
inset-inline-end: 1.2rem;
transition: background-color 0.2s ease;
}
@@ -776,44 +720,28 @@ onMounted(() => {
.search-input {
border-radius: 12px;
font-size: 16px;
padding-right: 40px;
}
.search-input :deep(.v-field__input) {
padding-top: 6px;
padding-bottom: 6px;
min-height: 40px;
}
.search-icon {
color: rgb(var(--v-theme-primary));
}
.search-list {
background-color: rgb(var(--v-theme-background));
}
.category-title {
font-size: 12px;
letter-spacing: 1px;
}
.option-icon-wrapper {
width: 32px;
height: 32px;
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.12);
margin-right: 12px;
block-size: 32px;
inline-size: 32px;
margin-inline-end: 12px;
}
.search-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
background-color: rgba(var(--v-theme-primary), 0.08);
block-size: 36px;
inline-size: 36px;
}
.search-icon-wrapper.warning {
@@ -825,9 +753,9 @@ onMounted(() => {
}
.search-option {
transition: transform 0.2s ease, background-color 0.2s ease;
margin-bottom: 2px;
border: 1px solid transparent;
margin-block-end: 2px;
transition: transform 0.2s ease, background-color 0.2s ease;
}
.search-option:hover {
@@ -836,19 +764,17 @@ onMounted(() => {
}
.recent-searches {
min-height: 200px;
background-color: rgb(var(--v-theme-background));
min-block-size: 200px;
}
.site-search-card {
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: 14px;
background-color: rgb(var(--v-theme-surface));
}
.site-chip {
transition: all 0.2s ease;
font-weight: normal;
transition: all 0.2s ease;
}
.site-chip:hover {
@@ -857,9 +783,9 @@ onMounted(() => {
}
.search-btn {
min-width: 70px;
font-weight: 500;
letter-spacing: 0.5px;
min-inline-size: 70px;
}
.empty-search-state,
@@ -872,6 +798,7 @@ onMounted(() => {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -887,10 +814,11 @@ onMounted(() => {
}
.site-select-btn {
min-height: 32px;
font-size: 12px;
letter-spacing: 0.5px;
padding: 0 12px;
min-block-size: 32px;
padding-block: 0;
padding-inline: 12px;
}
.site-chips-container {
@@ -898,7 +826,7 @@ onMounted(() => {
background-color: rgba(var(--v-theme-surface-variant), 0.06);
}
@media (max-width: 600px) {
@media (width <= 600px) {
.search-box-container {
padding: 16px;
}
@@ -908,19 +836,20 @@ onMounted(() => {
}
.close-btn {
right: 0.8rem;
top: 1rem;
width: 32px;
height: 32px;
block-size: 32px;
inline-size: 32px;
inset-block-start: 1rem;
inset-inline-end: 0.8rem;
}
.site-chips-container {
padding: 6px 8px;
padding-block: 6px;
padding-inline: 8px;
}
.site-select-btn {
min-height: 28px;
font-size: 11px;
min-block-size: 28px;
}
}
</style>

View File

@@ -8,14 +8,10 @@ const props = defineProps({
required: true,
},
selected: Array as PropType<Number[]>,
savebtn: {
type: Boolean,
default: false,
},
})
// 定义事件
const emit = defineEmits(['close', 'search', 'reload', 'save'])
const emit = defineEmits(['close', 'search', 'reload'])
// 过滤词
const siteFilter = ref('')
@@ -23,11 +19,14 @@ const siteFilter = ref('')
// 已选择站点
const selectedSites = ref<any[]>(props.selected || [])
watch(() => props.selected, value => {
if (selectedSites.value.length == 0 && value) {
selectedSites.value = value
}
})
watch(
() => props.selected,
value => {
if (selectedSites.value.length == 0 && value) {
selectedSites.value = value
}
},
)
// 全选/全不选按钮文字
const checkAllText = computed(() => {
@@ -171,16 +170,6 @@ const filteredSites = computed(() => {
>
取消
</VBtn>
<VBtn
v-if="savebtn"
color="success"
variant="flat"
@click="emit('save', selectedSites)"
class="mr-2 d-flex align-center justify-center"
:disabled="selectedSites.length === 0"
>
确定
</VBtn>
<VBtn
color="primary"
variant="flat"

View File

@@ -134,7 +134,7 @@ onMounted(() => {
<VToolbarTitle>{{ `浏览 - ${props.site?.name}` }}</VToolbarTitle>
<VSpacer />
<VToolbarItems>
<VBtn icon variant="plain" @click="emit('close')" class="me-3">
<VBtn icon @click="emit('close')" class="me-3">
<VIcon size="large" color="white" icon="ri-close-line" />
</VBtn>
</VToolbarItems>
@@ -238,17 +238,13 @@ onMounted(() => {
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="openTorrentDetail(item.page_url || '')">
<VListItem @click="openTorrentDetail(item.page_url || '')">
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="item.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile(item.enclosure)"
>
<VListItem v-if="item.enclosure?.startsWith('http')" @click="downloadTorrentFile(item.enclosure)">
<template #prepend>
<VIcon icon="mdi-download" />
</template>

View File

@@ -181,7 +181,6 @@ const dropdownItems = ref([
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.color"
@click="menu.props.click(item)"
>

View File

@@ -10,7 +10,7 @@ import WorkflowSidebar from '@/layouts/components/WorkflowSidebar.vue'
import DropzoneBackground from '@/layouts/components/DropzoneBackground.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
const { onConnect, addEdges, nodes, edges } = useVueFlow()
const { onConnect, addEdges, nodes, edges, addNodes, screenToFlowCoordinate } = useVueFlow()
const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
@@ -98,6 +98,43 @@ const $toast = useToast()
// 导入代码对话框
const importCodeDialog = ref(false)
// 为移动端生成节点ID
function getId() {
return 'act_' + Math.random().toString(36).substr(2, 9)
}
// 处理移动端组件点击事件
function handleComponentClick(action: any) {
// 计算当前视图中心点
const centerX = window.innerWidth / 2
const centerY = window.innerHeight / 3
// 转换为画布坐标
const position = screenToFlowCoordinate({
x: centerX,
y: centerY,
})
// 生成一个新节点ID
const nodeId = getId()
// 创建新节点
const newNode = {
id: nodeId,
type: action.type,
name: action.name,
description: action.desc || '',
position,
data: {},
}
// 添加节点到画布
addNodes(newNode)
// 显示提示
$toast.success('已添加组件到画布')
}
// 调用API 编辑任务
async function updateWorkflow() {
// 更新节点和流程
@@ -157,32 +194,31 @@ const isMacOS = computed(() => {
<template>
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
<VCard>
<VCard class="workflow-dialog">
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbarItems>
<VBtn icon @click="emit('close')" class="ms-3">
<VIcon size="large" color="white" icon="mdi-close" />
</VBtn>
</VToolbarItems>
<VToolbarTitle> 编辑流程 - {{ workflow?.name }} </VToolbarTitle>
<VToolbarItems>
<VBtn icon @click="importCodeDialog = true">
<VIcon size="large" color="white" icon="mdi-import" />
</VBtn>
<VBtn icon @click="shareWorkflow">
<VIcon size="large" color="white" icon="mdi-share" />
</VBtn>
<VBtn icon @click="updateWorkflow" class="mx-5">
<VIcon size="large" color="white" icon="mdi-content-save" />
</VBtn>
</VToolbarItems>
</VToolbar>
</div>
<VDivider />
<VCardText class="px-0 py-0">
<div class="dnd-flow" @drop="onDrop">
<VToolbar color="primary">
<VToolbarItems>
<VBtn icon @click="emit('close')" class="ms-3">
<VIcon size="large" color="white" icon="mdi-close" />
</VBtn>
</VToolbarItems>
<VToolbarTitle> 编辑流程 - {{ workflow?.name }} </VToolbarTitle>
<VSpacer></VSpacer>
<VToolbarItems>
<VBtn icon variant="text" @click="importCodeDialog = true" class="ms-2">
<VIcon size="24" color="white" icon="mdi-import" />
</VBtn>
<VBtn icon variant="text" @click="shareWorkflow" class="ms-2">
<VIcon size="24" color="white" icon="mdi-share" />
</VBtn>
<VBtn icon variant="text" @click="updateWorkflow" class="ms-2 me-3">
<VIcon size="24" color="white" icon="mdi-content-save" />
</VBtn>
</VToolbarItems>
</VToolbar>
<VCardText class="workflow-content pa-0">
<div class="workflow-canvas" @drop="onDrop">
<VueFlow
:nodes="nodes"
:edges="edges"
@@ -204,10 +240,11 @@ const isMacOS = computed(() => {
>
</DropzoneBackground>
</VueFlow>
<WorkflowSidebar />
<WorkflowSidebar @component-click="handleComponentClick" />
</div>
</VCardText>
</VCard>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
@@ -218,88 +255,43 @@ const isMacOS = computed(() => {
/>
</VDialog>
</template>
<style>
<style lang="scss">
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
@import '@vue-flow/controls/dist/style.css';
@import '@vue-flow/minimap/dist/style.css';
@import '@vue-flow/node-resizer/dist/style.css';
.vue-flow__minimap {
transform: scale(75%);
transform-origin: bottom right;
}
.dnd-flow {
.workflow-dialog {
display: flex;
overflow: hidden;
flex-direction: column;
block-size: 100%;
}
.dnd-flow aside {
background: #10b981bf;
border-inline-end: 1px solid #eee;
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 30%);
box-shadow: 0 5px 10px #0000004d;
color: #fff;
font-size: 12px;
font-weight: 700;
padding-block: 15px;
padding-inline: 10px;
.workflow-content {
position: relative;
overflow: hidden;
flex: 1;
}
.dnd-flow aside .nodes > * {
box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 25%);
box-shadow: 5px 5px 10px 2px #00000040;
cursor: grab;
font-weight: 500;
margin-block-end: 10px;
}
.dnd-flow aside .description {
margin-block-end: 10px;
}
.dnd-flow .vue-flow-wrapper {
flex-grow: 1;
block-size: 100%;
}
@media screen and (width >= 640px) {
.dnd-flow {
flex-direction: row;
}
.dnd-flow aside {
max-inline-size: 25%;
}
}
@media screen and (width <= 639px) {
.dnd-flow aside .nodes {
display: flex;
flex-direction: row;
gap: 5px;
}
}
.dropzone-background {
.workflow-canvas {
position: relative;
block-size: 100%;
inline-size: 100%;
}
.dropzone-background .overlay {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
pointer-events: none;
.vue-flow__minimap {
overflow: hidden;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
background-color: rgba(var(--v-theme-surface), 0.8);
box-shadow: 0 4px 15px rgba(var(--v-shadow-key-umbra-color), 0.1);
inset-block-end: 20px;
inset-inline-end: 20px;
transform: scale(75%);
transform-origin: bottom right;
}
.vue-flow__handle {
@@ -320,4 +312,39 @@ const isMacOS = computed(() => {
.vue-flow__handle-right {
background-color: rgb(var(--v-theme-error));
}
// 自定义节点样式
.vue-flow__node {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 12px;
&:hover {
box-shadow: 0 8px 16px rgba(var(--v-shadow-key-umbra-color), 0.15) !important;
transform: translateY(-2px);
}
&.selected {
box-shadow: 0 0 0 1px rgb(var(--v-theme-primary)) !important;
}
}
// 自定义动作连线样式
.vue-flow__edge.animation {
.vue-flow__edge-path {
stroke: rgb(var(--v-theme-primary));
}
&.selected {
.vue-flow__edge-path {
stroke: rgb(var(--v-theme-primary));
stroke-width: 4;
}
}
}
@media screen and (width <= 600px) {
.vue-flow__minimap {
display: none;
}
}
</style>

View File

@@ -645,12 +645,7 @@ onMounted(() => {
<VMenu activator="parent" close-on-content-click>
<VList>
<template v-for="(menu, i) in dropdownItems" :key="i">
<VListItem
v-if="menu.show"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
<VListItem v-if="menu.show" :base-color="menu.props.color" @click="menu.props.click(item)">
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
@@ -661,41 +656,21 @@ onMounted(() => {
</VMenu>
</IconBtn>
<span v-if="hover.isHovering && display.mdAndUp.value && !selectMode" class="flex">
<VTooltip text="识别">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="刮削">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="scrape(item)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="重命名">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="整理">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="删除">
<template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</template>
</VTooltip>
<IconBtn @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn @click.stop="scrape(item)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
<IconBtn @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</span>
</template>
</VListItem>

View File

@@ -313,14 +313,9 @@ function getIndentLevel(path: string, ancestorPath: string) {
:color="currentPath === directory.path ? 'primary' : 'amber-darken-1'"
class="me-1"
/>
<VTooltip :disabled="directory.name.length <= 18">
<template #activator="{ props: tooltipProps }">
<span class="folder-name" v-bind="tooltipProps">
{{ directory.name }}
</span>
</template>
<span class="folder-name">
{{ directory.name }}
</VTooltip>
</span>
</div>
</div>
@@ -367,14 +362,9 @@ function getIndentLevel(path: string, ancestorPath: string) {
:color="currentPath === item.dir.path ? 'primary' : 'amber-darken-1'"
class="me-1"
/>
<VTooltip :disabled="item.dir.name.length <= 18">
<template #activator="{ props: tooltipProps }">
<span class="folder-name" v-bind="tooltipProps">
{{ item.dir.name }}
</span>
</template>
<span class="folder-name">
{{ item.dir.name }}
</VTooltip>
</span>
</div>
</div>
</div>

View File

@@ -119,7 +119,7 @@ const sortIcon = computed(() => {
<VToolbarItems class="overflow-hidden">
<VMenu v-if="inProps.storages?.length || 0 > 1" offset-y>
<template #activator="{ props }">
<VBtn v-bind="props">
<VBtn>
<VIcon icon="mdi-arrow-down-drop-circle-outline" />
</VBtn>
</template>
@@ -155,28 +155,16 @@ const sortIcon = computed(() => {
</template>
</VToolbarItems>
<div class="flex-grow-1" />
<VTooltip text="调整排序">
<template #activator="{ props }">
<IconBtn v-bind="props" @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="返回上一级" v-if="pathSegments.length > 0">
<template #activator="{ props }">
<IconBtn v-bind="props" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
</template>
</VTooltip>
<IconBtn @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
<IconBtn @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<VDialog v-model="newFolderPopper" max-width="50rem">
<template #activator="{ props }">
<IconBtn v-bind="props">
<VTooltip text="新建文件夹">
<template #activator="{ props: _props }">
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
</template>
</VTooltip>
<IconBtn>
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
</IconBtn>
</template>
<VCard title="新建文件夹">

View File

@@ -1,13 +1,10 @@
<script lang="ts" setup>
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
import { ref, onMounted, onUnmounted, inject, computed } from 'vue'
// 元素
const slideview_content = ref()
const sliderContainer = ref()
const slideview_content = ref<HTMLElement | null>(null)
const sliderContainer = ref<HTMLElement | null>(null)
// 分页切换状态: 0-左边不可用 1-两边可用 2-右边不可用 3-两边都不可用
const disabled = ref(0)
// 记录滚动值
@@ -22,10 +19,11 @@ let card_width: number
let card_max: number
// 当前定位
let card_current: number
// 是否鼠标悬停在容器上
const isHovering = ref(false)
// 获取传入的链接地址
const props: any = inject('rankingPropsKey', { linkurl: '', title: '' })
const isScrolling = ref(false)
let scrollTimeout: ReturnType<typeof setTimeout> | null = null
const scrollTimeoutDuration = 1500 // 滚动停止后延迟时间 (ms)
// 分页切换
function slideNext(next: boolean) {
@@ -33,18 +31,27 @@ function slideNext(next: boolean) {
if (next) {
const card_index = card_current + card_max
run_to_left_px = card_index * card_width
if (run_to_left_px >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth)
run_to_left_px = slideview_content.value.scrollWidth - slideview_content.value.clientWidth
if (run_to_left_px >= slideview_content.value!.scrollWidth - slideview_content.value!.clientWidth)
run_to_left_px = slideview_content.value!.scrollWidth - slideview_content.value!.clientWidth
} else {
const card_index = card_current - card_max
run_to_left_px = card_index * card_width
if (run_to_left_px <= 0) run_to_left_px = 0
}
slideview_content.value.scrollTo({
slideview_content.value!.scrollTo({
top: 0,
left: run_to_left_px,
behavior: 'smooth',
})
// 点击后强制显示并重置计时器
isScrolling.value = true
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
scrollTimeout = setTimeout(() => {
isScrolling.value = false
}, scrollTimeoutDuration)
}
// 计算最大显示数量
@@ -58,8 +65,25 @@ function countMaxNumber() {
countDisabled()
}
// 修改分页切换按钮状态
// 修改分页切换按钮状态 & 处理滚动状态
function handleContentScroll() {
if (!slideview_content.value) return
// 更新按钮禁用状态
countDisabled()
// 更新滚动状态并重置计时器
isScrolling.value = true
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
scrollTimeout = setTimeout(() => {
isScrolling.value = false
}, scrollTimeoutDuration) // 使用常量
}
// 原始的 countDisabled 逻辑,现在由 handleContentScroll 调用
function countDisabled() {
if (!slideview_content.value) return
slideview_scrollLeft.value = slideview_content.value.scrollLeft
card_current =
slideview_content.value.scrollLeft === 0
@@ -75,21 +99,6 @@ function countDisabled() {
else disabled.value = 1
}
// 处理鼠标进入
function handleMouseEnter() {
isHovering.value = true
}
// 处理鼠标离开
function handleMouseLeave() {
isHovering.value = false
}
// 检测是否有足够内容可显示
const hasEnoughContent = computed(() => {
return slide_card_length > card_max
})
// 组件加载完成
onMounted(() => {
// 初次获取元素参数
@@ -105,64 +114,49 @@ onUnmounted(() => {
onActivated(() => {
if (slideview_scrollLeft.value !== 0) {
slideview_content.value.scrollLeft = slideview_scrollLeft.value
slideview_content.value!.scrollLeft = slideview_scrollLeft.value
}
})
</script>
<template>
<div
ref="sliderContainer"
class="slider-container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div ref="sliderContainer" class="slider-container" :class="{ 'is-scrolling': isScrolling }">
<div class="slider-header">
<slot name="title">
<SlideViewTitle />
</slot>
<!-- 查看全部按钮 -->
<RouterLink
v-if="props.linkurl"
:to="props.linkurl"
class="view-all-button"
>
<span>全部</span>
<RouterLink v-if="props.linkurl" :to="props.linkurl" class="view-all-button">
<span>更多</span>
<svg width="16" height="16" viewBox="0 0 24 24" class="arrow-svg">
<path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
</svg>
</RouterLink>
</div>
<div class="slider-content-wrapper">
<div class="slider-content-container">
<div
ref="slideview_content"
class="slider-content"
tabindex="0"
@scroll="countDisabled"
>
<div ref="slideview_content" class="slider-content" tabindex="0" @scroll="handleContentScroll">
<slot name="content" />
</div>
</div>
<!-- 左侧导航按钮 -->
<button
<button
class="nav-button nav-button-left"
@click.stop="slideNext(false)"
v-show="isHovering && disabled !== 0 && disabled !== 3"
v-show="disabled !== 0 && disabled !== 3"
>
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" />
</svg>
</button>
<!-- 右侧导航按钮 -->
<button
<button
class="nav-button nav-button-right"
@click.stop="slideNext(true)"
v-show="isHovering && disabled !== 2 && disabled !== 3"
v-show="disabled !== 2 && disabled !== 3"
>
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
@@ -175,104 +169,106 @@ onActivated(() => {
<style lang="scss" scoped>
.slider-container {
position: relative;
margin-bottom: 24px;
// 移除padding按钮放置在外部
margin-block-end: 24px;
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 0 8px;
justify-content: space-between;
gap: 16px;
margin-block-end: 12px;
padding-block: 0;
padding-inline: 8px;
& > :first-child {
flex-grow: 1;
min-width: 0;
min-inline-size: 0;
}
}
.view-all-button {
.arrow-svg {
fill: currentColor;
transition: transform 0.3s ease;
margin-left: 2px;
}
display: inline-flex;
flex-shrink: 0;
align-items: center;
text-decoration: none;
border-radius: 8px;
padding: 5px 12px;
background-color: transparent;
color: rgb(var(--v-theme-primary));
font-size: 0.85rem;
font-weight: 500;
color: rgb(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.1);
padding: 4px 10px;
border-radius: 16px;
text-decoration: none;
transition: all 0.25s ease;
flex-shrink: 0;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.15);
box-shadow: 0 2px 8px rgba(var(--v-theme-primary), 0.1);
background-color: rgba(var(--v-theme-primary), 0.08);
border-color: rgba(var(--v-theme-primary), 0.5);
transform: translateY(-1px);
.arrow-svg {
transform: translateX(2px);
transform: translateX(3px);
}
}
span {
margin-right: 4px;
}
.arrow-svg {
transition: transform 0.3s ease;
fill: currentColor;
margin-inline-end: 4px;
}
}
.slider-content-wrapper {
position: relative;
width: 100%;
inline-size: 100%;
}
.slider-content-container {
position: relative;
width: 100%;
overflow: hidden;
inline-size: 100%;
}
.nav-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 38px;
height: 38px;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: rgba(var(--v-theme-surface), 0.9);
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
background-color: rgba(var(--v-theme-background), 0.8);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 20;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.3s ease, box-shadow 0.3s ease;
padding: 0;
color: rgb(var(--v-theme-on-surface));
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.3s ease,
box-shadow 0.3s ease, border-color 0.3s ease;
svg {
fill: rgb(var(--v-theme-on-surface));
opacity: 0.8;
fill: currentColor;
opacity: 0.7;
transition: all 0.3s ease;
width: 22px;
height: 22px;
filter: none;
}
&:hover {
transform: translateY(-50%) scale(1.1);
background-color: rgba(var(--v-theme-surface), 1);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-background), 0.95);
transform: translateY(-50%) scale(1.05);
color: rgb(var(--v-theme-primary));
svg {
opacity: 1;
}
@@ -280,39 +276,51 @@ onActivated(() => {
}
.nav-button-left {
left: -19px; // 半径
left: 8px;
}
.nav-button-right {
right: -19px; // 半径
right: 8px;
}
.slider-content {
display: grid;
grid-template-rows: 1fr;
grid-auto-flow: column;
overflow: scroll hidden !important;
justify-content: start;
gap: 16px;
padding: 8px 12px;
overflow: scroll hidden !important;
grid-auto-flow: column;
grid-template-rows: 1fr;
-ms-overflow-style: none !important;
overscroll-behavior-x: contain !important;
scrollbar-width: none !important;
padding-block: 8px;
padding-inline: 12px;
scroll-behavior: smooth;
scrollbar-width: none !important;
&::-webkit-scrollbar {
display: none;
}
}
.slider-container:hover .nav-button[style*="display: none;"] ~ .nav-button,
.slider-container:hover .nav-button {
// 触摸设备:滚动时显示 (通过 JS 添加的类控制)
// 这个规则会在不支持 hover 的设备上生效
.slider-container.is-scrolling .nav-button {
opacity: 1;
pointer-events: auto;
}
.nav-button[style*="display: none;"] {
opacity: 0 !important;
pointer-events: none !important;
// 桌面设备:悬停时显示
@media (hover: hover) {
.slider-container:hover .nav-button {
// 这个规则会覆盖 .is-scrolling 的效果 (如果同时存在)
// 或者在非 scrolling 状态下hover 时也能显示
opacity: 1;
pointer-events: auto;
}
// 在 hover 设备上,即使在滚动,如果鼠标不悬停,按钮也应该隐藏
// 因此,基础 .nav-button 的 opacity: 0 规则在这里仍然是必要的
// (之前错误地以为 hover 会完全覆盖,但滚动时 class 和 hover 可能同时存在)
// .nav-button { opacity: 0; pointer-events: none; } // 这行其实不需要重复,默认就是这样
}
</style>

View File

@@ -15,30 +15,30 @@ const props: any = inject('rankingPropsKey')
<style lang="scss" scoped>
.title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
justify-content: space-between;
inline-size: 100%;
}
.title-section {
position: relative;
display: flex;
align-items: center;
position: relative;
}
.title-badge {
width: 3px;
height: 16px;
background-color: rgb(var(--v-theme-primary));
margin-right: 8px;
border-radius: 2px;
background-color: rgb(var(--v-theme-primary));
block-size: 16px;
inline-size: 3px;
margin-inline-end: 8px;
}
.title-text {
font-size: 1.1rem;
font-weight: 600;
color: rgba(var(--v-theme-on-background), 0.95);
margin: 0;
padding: 0;
margin: 0;
color: rgba(var(--v-theme-on-background), 0.95);
font-size: 1.2rem;
font-weight: 600;
}
</style>

View File

@@ -44,7 +44,7 @@ onMounted(() => {
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-download-box-outline" size="x-large"></VIcon>
<VIcon icon="mdi-download" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>添加下载</VCardTitle>

View File

@@ -19,7 +19,7 @@ defineProps({
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-star-check" size="x-large"></VIcon>
<VIcon icon="mdi-star-plus" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>添加订阅</VCardTitle>

View File

@@ -110,7 +110,7 @@ onMounted(() => {
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-multimedia" size="x-large"></VIcon>
<VIcon icon="mdi-movie-search" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>获取媒体数据</VCardTitle>

View File

@@ -20,7 +20,7 @@ defineProps({
<VCardItem>
<template v-slot:prepend>
<VAvatar>
<VIcon icon="mdi-file-move" size="x-large"></VIcon>
<VIcon icon="mdi-folder-search" size="x-large"></VIcon>
</VAvatar>
</template>
<VCardTitle>扫描目录</VCardTitle>

View File

@@ -60,11 +60,11 @@ const currentPath = computed(() => route.path)
:icon="moreMenuDialog ? 'mdi-close' : 'mdi-dots-horizontal'"
:color="moreActiveState ? 'primary' : ''"
/>
<VMenu v-model="moreMenuDialog" close-on-content-click activator="parent">
<VList class="font-bold" lines="one" elevation="1">
<VMenu v-model="moreMenuDialog" close-on-content-click activator="parent" scrim>
<VList class="font-bold" lines="one">
<VListSubheader class="bg-transparent"> 更多 </VListSubheader>
<VListItem
class="pe-20"
class="pe-20 ps-5"
v-for="(menu, index) in moreMemus"
:key="index"
:prepend-icon="menu.icon"

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: String,
default: '* * * * *',
},
items: {
type: Array as PropType<{ title: string; icon: string }[]>,
default: () => [],
},
})
const emit = defineEmits(['update:modelValue'])
const currentValue = ref(props.modelValue)
watch(currentValue, newVal => {
emit('update:modelValue', newVal)
})
watch(
() => props.modelValue,
value => {
currentValue.value = value
},
)
// Ref for the tabs container
const tabsContainerRef = ref<HTMLElement | null>(null)
// State for showing the scroll indicator
const showTabsScrollIndicator = ref(false)
// Function to check and update the indicator state
const updateTabsIndicator = () => {
const el = tabsContainerRef.value
if (!el) return
const tolerance = 1 // Allow 1px tolerance
const hasOverflow = el.scrollWidth > el.clientWidth + tolerance
const isScrolledToEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - tolerance
showTabsScrollIndicator.value = hasOverflow && !isScrolledToEnd
}
// Debounce resize handler
let resizeTimeout: ReturnType<typeof setTimeout> | null = null
const handleResize = () => {
if (resizeTimeout) clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(() => {
updateTabsIndicator()
}, 150)
}
onMounted(async () => {
// Add resize listener for tabs indicator
window.addEventListener('resize', handleResize)
// Initial check for tabs indicator after DOM update
await nextTick() // Ensure element is rendered
updateTabsIndicator()
// Listen for scroll events specifically on the tabs container
tabsContainerRef.value?.addEventListener('scroll', updateTabsIndicator, { passive: true })
})
onUnmounted(() => {
// Remove resize listener
window.removeEventListener('resize', handleResize)
// Remove tabs scroll listener
tabsContainerRef.value?.removeEventListener('scroll', updateTabsIndicator)
})
</script>
<template>
<div class="tab-header">
<div ref="tabsContainerRef" class="header-tabs" :class="{ 'show-indicator': showTabsScrollIndicator }">
<div
v-for="(item, index) in items"
:key="index"
class="header-tab"
:class="{ 'active': currentValue === item.title }"
@click="currentValue = item.title"
>
<VIcon v-if="item.icon" :icon="item.icon" size="small" class="header-tab-icon" />
<span>{{ item.title }}</span>
</div>
</div>
<slot name="append" />
</div>
</template>
<style scoped lang="scss">
.tab-header {
position: sticky;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.8);
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.05);
gap: 16px; // 为按钮留出空间
inset-block-start: 0;
margin-block-end: 16px;
padding-block: 8px;
padding-inline: 16px;
}
.header-tabs {
position: relative; // Needed for pseudo-element positioning
display: flex;
flex-grow: 1;
gap: 12px;
// Clip content that overflows, useful with padding
mask-image: linear-gradient(to right, black 95%, transparent 100%);
min-inline-size: 0;
overflow-x: auto;
padding-block: 4px;
padding-inline: 0;
// Add padding-right to make space for the indicator visually
padding-inline-end: 20px;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
// Gradient indicator pseudo-element
&::after {
position: absolute;
z-index: 1; // Ensure it's above the tabs but below other header elements if needed
background: linear-gradient(to left, rgba(var(--v-theme-background), 1) 30%, transparent);
content: '';
inline-size: 40px; // Width of the fade effect
inset-block: 0;
inset-inline-end: 0;
opacity: 0; // Hidden by default
pointer-events: none; // Allow interaction with content behind it
transition: opacity 0.2s ease-in-out;
}
// Show the indicator when the class is present
&.show-indicator::after {
opacity: 1;
}
}
.header-tab-icon {
color: rgba(var(--v-theme-on-background), 0.6);
margin-inline-end: 6px;
transition: color 0.2s ease;
}
.header-tab {
position: relative;
display: flex;
align-items: center;
border-radius: 20px;
background-color: transparent;
color: rgba(var(--v-theme-on-background), 0.7);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
padding-block: 6px;
padding-inline: 14px;
transition: all 0.2s ease;
white-space: nowrap;
&::after {
position: absolute;
border-radius: 3px;
background-color: rgb(var(--v-theme-primary));
block-size: 3px;
content: '';
inline-size: 70%;
inset-block-end: -4px;
inset-inline-start: 50%;
transform: translateX(-50%) scaleX(0);
transition: transform 0.2s ease;
}
&.active {
color: rgb(var(--v-theme-primary));
&::after {
transform: translateX(-50%) scaleX(1);
}
.header-tab-icon {
color: rgb(var(--v-theme-primary));
}
}
&:hover:not(.active) {
background-color: rgba(var(--v-theme-primary), 0.05);
color: rgba(var(--v-theme-on-background), 1);
}
}
</style>

View File

@@ -113,6 +113,7 @@ onMounted(() => {
transition="scale-transition"
close-on-content-click
close-on-back
scrim
>
<!-- Menu Activator -->
<template #activator="{ props }">
@@ -121,7 +122,7 @@ onMounted(() => {
</IconBtn>
</template>
<!-- Menu Content -->
<VCard elevation="1" class="shortcut-menu-card">
<VCard class="shortcut-menu-card">
<VCardItem class="shortcut-header border-b">
<VCardTitle class="font-weight-medium text-primary">捷径</VCardTitle>
<template #append>
@@ -202,9 +203,16 @@ onMounted(() => {
</VCard>
</VMenu>
<!-- 名称测试弹窗 -->
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="50rem" scrollable>
<VCard title="名称识别测试">
<DialogCloseBtn @click="nameTestDialog = false" />
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-text-recognition" class="me-2" />
名称识别测试
</VCardTitle>
<DialogCloseBtn @click="nameTestDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<NameTestView />
</VCardText>
@@ -212,8 +220,14 @@ onMounted(() => {
</VDialog>
<!-- 网络测试弹窗 -->
<VDialog v-if="netTestDialog" v-model="netTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VCard title="网络测试">
<DialogCloseBtn @click="netTestDialog = false" />
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-network" class="me-2" />
网速连通性测试
</VCardTitle>
<DialogCloseBtn @click="netTestDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<NetTestView />
@@ -232,6 +246,7 @@ onMounted(() => {
<DialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="inline-flex">
<VIcon icon="mdi-file-document" class="me-2" />
实时日志
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
<div
@@ -250,9 +265,16 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 规则测试弹窗 -->
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="50rem" scrollable>
<VCard title="规则测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="40rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-filter-cog" class="me-2" />
规则测试
</VCardTitle>
<DialogCloseBtn @click="ruleTestDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<RuleTestView />
</VCardText>
@@ -260,8 +282,14 @@ onMounted(() => {
</VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog v-if="systemTestDialog" v-model="systemTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VCard title="系统健康检查">
<DialogCloseBtn @click="systemTestDialog = false" />
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-cog" class="me-2" />
系统健康检查
</VCardTitle>
<DialogCloseBtn @click="systemTestDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<ModuleTestView />
@@ -276,8 +304,14 @@ onMounted(() => {
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard title="消息中心">
<DialogCloseBtn @click="messageDialog = false" />
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-message" class="me-2" />
消息中心
</VCardTitle>
<DialogCloseBtn @click="messageDialog = false" />
</VCardItem>
<VDivider />
<VCardText ref="chatContainer">
<MessageView @scroll="scrollMessageToEnd" />
@@ -356,7 +390,6 @@ onMounted(() => {
padding: 16px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
border-radius: 12px;
background-color: rgba(var(--v-theme-surface));
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

View File

@@ -41,7 +41,7 @@ onBeforeUnmount(() => {
</script>
<template>
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click class="notification-menu">
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click class="notification-menu" scrim>
<!-- Menu Activator -->
<template #activator="{ props }">
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
@@ -54,7 +54,7 @@ onBeforeUnmount(() => {
</IconBtn>
</template>
<!-- Menu Content -->
<VCard elevation="0">
<VCard>
<VCardItem class="notification-header">
<VCardTitle class="font-weight-medium text-primary">通知中心</VCardTitle>
<template #append>
@@ -119,20 +119,8 @@ onBeforeUnmount(() => {
}
.notification-item {
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
border-radius: 12px;
margin-block-end: 8px;
transition: all 0.2s ease;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.03);
box-shadow: 0 4px 8px rgba(var(--v-theme-on-surface), 0.06);
transform: translateY(-2px);
}
.notification-avatar {
background-color: rgba(var(--v-theme-primary), 0.1);
box-shadow: 0 4px 8px rgba(var(--v-theme-primary), 0.15);
}
.notification-title {

View File

@@ -85,8 +85,8 @@ const userLevel = computed(() => userStore.level)
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar" />
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu">
<VList elevation="0" class="overflow-hidden">
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu" scrim>
<VList class="overflow-hidden pt-0">
<!-- 👉 User Avatar & Name -->
<div class="user-profile-header px-2 py-4 mb-2">
<div class="d-flex align-center">

View File

@@ -1,11 +1,29 @@
<script lang="ts" setup>
import api from '@/api'
import useDragAndDrop from '@core/utils/workflow'
import { useDisplay } from 'vuetify'
interface ActionItem {
name: string
type: string
desc?: string
}
const display = useDisplay()
// APP
const appMode = inject('pwaMode') && display.mdAndDown.value
const { onDragStart } = useDragAndDrop()
// 组件列表
const actions = ref([])
const actions = ref<ActionItem[]>([])
// 侧边栏是否收起 (仅在桌面端有效)
const isSidebarCollapsed = ref(false)
// 侧边栏在移动端是否显示
const showMobileSidebar = ref(false)
// 定义emit
const emit = defineEmits(['component-click'])
// 加载组件列表
async function load_actions() {
@@ -16,25 +34,382 @@ async function load_actions() {
}
}
// 切换侧边栏收起状态
function toggleSidebar() {
isSidebarCollapsed.value = !isSidebarCollapsed.value
}
// 切换移动端侧边栏显示状态
function toggleMobileSidebar() {
showMobileSidebar.value = !showMobileSidebar.value
}
// 处理移动端点击组件事件
function handleComponentClick(action: ActionItem) {
// 向父组件发送事件
emit('component-click', action)
// 关闭侧边栏
showMobileSidebar.value = false
}
// 根据动作类型获取图标
function getActionIcon(type: string): string {
const iconMap: Record<string, string> = {
'AddSubscribeAction': 'mdi-star-plus',
'AddDownloadAction': 'mdi-download',
'FetchDownloadsAction': 'mdi-progress-download',
'FetchMediasAction': 'mdi-movie-search',
'FetchRssAction': 'mdi-rss',
'FetchTorrentsAction': 'mdi-search-web',
'FilterMediasAction': 'mdi-filter-check',
'FilterTorrentsAction': 'mdi-filter-multiple',
'ScanFileAction': 'mdi-folder-search',
'ScrapeFileAction': 'mdi-file-find',
'SendEventAction': 'mdi-send-check',
'SendMessageAction': 'mdi-message-arrow-right',
'TransferFileAction': 'mdi-file-move',
}
return iconMap[type] || 'mdi-puzzle-outline'
}
// 计算侧边栏类名
const sidebarClasses = computed(() => {
return {
'sidebar-collapsed': isSidebarCollapsed.value && !display.smAndDown.value,
'sidebar-mobile': display.smAndDown.value,
'sidebar-mobile-open': showMobileSidebar.value && display.smAndDown.value,
}
})
// 监听屏幕尺寸变化,自动关闭移动端侧边栏
watch(
() => display.smAndDown.value,
isMobile => {
if (!isMobile) {
showMobileSidebar.value = false
}
},
)
onMounted(() => {
load_actions()
})
</script>
<template>
<aside>
<div class="mb-3"><VLabel>可选动作组件</VLabel></div>
<!-- 移动端触发按钮 -->
<div
v-if="display.smAndDown.value"
class="workflow-sidebar-trigger"
:class="appMode ? 'right-4 bottom-28' : 'right-4 bottom-4'"
@click="toggleMobileSidebar"
>
<VBtn icon size="large" class="workflow-sidebar-fab">
<VIcon :icon="showMobileSidebar ? 'mdi-close' : 'mdi-plus'" />
</VBtn>
</div>
<div class="nodes flex flex-wrap justify-center">
<div
class="vue-flow__node-default cursor-grab mx-1"
v-for="(action, index) in actions"
:key="index"
:draggable="true"
@dragstart="onDragStart($event, action)"
>
{{ action['name'] }}
<!-- 侧边栏 -->
<aside class="workflow-sidebar" :class="sidebarClasses">
<div class="sidebar-container">
<!-- 侧边栏头部 -->
<div class="sidebar-header">
<div class="header-content">
<VAvatar size="36" class="workflow-logo">
<VIcon icon="mdi-puzzle" />
</VAvatar>
<span v-if="!isSidebarCollapsed || display.smAndDown.value" class="header-title">动作组件</span>
<IconBtn v-if="!display.smAndDown.value" @click="toggleSidebar" class="collapse-btn">
<VIcon :icon="isSidebarCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left'" />
</IconBtn>
</div>
</div>
<!-- 组件列表 -->
<div class="components-container">
<div
v-for="(action, index) in actions"
:key="index"
class="component-item"
:draggable="!display.smAndDown.value"
@dragstart="!display.smAndDown.value && onDragStart($event, action)"
@click="display.smAndDown.value && handleComponentClick(action)"
>
<div class="component-card">
<VAvatar size="36" class="component-avatar">
<VIcon :icon="getActionIcon(action.type)" size="18" />
</VAvatar>
<div v-if="!isSidebarCollapsed || display.smAndDown.value" class="component-info">
<div class="component-name">{{ action.name }}</div>
<div class="component-desc">{{ display.smAndDown.value ? '点击添加' : '拖动到画布' }}</div>
</div>
</div>
</div>
</div>
<!-- 底部提示 -->
<div class="sidebar-footer">
<VBtn block class="drag-btn">
<div class="btn-content">
<VIcon v-if="isSidebarCollapsed && !display.smAndDown.value" class="footer-icon" icon="mdi-gesture-swipe" />
<template v-else>
<VIcon :icon="display.smAndDown.value ? 'mdi-gesture-tap' : 'mdi-gesture-swipe'" class="me-2" />
<span>{{ display.smAndDown.value ? '点击组件添加到画布' : '拖动组件到画布' }}</span>
</template>
</div>
</VBtn>
</div>
</div>
</aside>
</template>
<style lang="scss" scoped>
@use 'sass:color';
.workflow-sidebar {
position: absolute;
z-index: 100;
overflow: hidden;
background-color: #f5f5f7;
box-shadow: 0 0 15px rgba(0, 0, 0, 8%);
inline-size: 280px;
inset-block: 0;
inset-inline-start: 0;
transition: all 0.3s ease;
&.sidebar-collapsed {
inline-size: 70px;
}
&.sidebar-mobile {
inline-size: 240px;
transform: translateX(-100%);
&.sidebar-mobile-open {
transform: translateX(0);
}
}
}
.sidebar-container {
display: flex;
flex-direction: column;
block-size: 100%;
}
.sidebar-header {
flex-shrink: 0;
padding: 16px;
background-color: #fff;
border-block-end: 1px solid rgba(0, 0, 0, 6%);
.header-content {
position: relative;
display: flex;
align-items: center;
}
.workflow-logo {
background-color: #8c58f5;
color: white;
margin-inline-end: 10px;
}
.header-title {
color: #1a1a1a;
font-size: 18px;
font-weight: 600;
}
.collapse-btn {
position: absolute;
color: #8c58f5;
inset-block-start: 0;
inset-inline-end: 0;
}
}
.components-container {
flex: 1;
padding: 12px;
overflow-y: auto;
&::-webkit-scrollbar {
inline-size: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: rgba(140, 88, 245, 30%);
}
}
.component-item {
cursor: grab;
margin-block-end: 10px;
&:active {
cursor: grabbing;
}
}
.component-card {
display: flex;
align-items: center;
padding: 10px;
border-radius: 12px;
background-color: #e4e4e7;
transition: all 0.2s ease;
&:hover {
background-color: #d4d4d8;
transform: translateY(-2px);
}
}
.component-avatar {
flex-shrink: 0;
background-color: #8c58f5;
color: white;
margin-inline-end: 12px;
.v-icon {
color: white !important;
opacity: 1 !important;
}
}
.component-info {
overflow: hidden;
max-inline-size: calc(100% - 48px);
}
.component-name {
overflow: hidden;
color: #1a1a1a;
font-size: 14px;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.component-desc {
overflow: hidden;
color: #71717a;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-footer {
flex-shrink: 0;
padding: 12px;
background-color: #fff;
border-block-start: 1px solid rgba(0, 0, 0, 6%);
.drag-btn {
background-color: #8c58f5;
block-size: 44px;
color: white;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
&:hover {
background-color: color.adjust(#8c58f5, $lightness: -5%);
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
inline-size: 100%;
}
.footer-icon {
font-size: 20px;
}
}
}
// 移动端悬浮按钮
.workflow-sidebar-trigger {
position: fixed;
z-index: 100;
}
.workflow-sidebar-fab {
background-color: #8c58f5;
box-shadow: 0 4px 10px rgba(140, 88, 245, 40%);
color: white;
&:hover {
background-color: color.adjust(#8c58f5, $lightness: -5%);
}
}
.sidebar-collapsed {
.component-card {
justify-content: center;
padding: 8px;
}
.component-avatar {
block-size: 40px !important;
inline-size: 40px !important;
margin-inline-end: 0;
.v-icon {
font-size: 20px !important;
}
}
.sidebar-footer {
padding-block: 10px;
padding-inline: 6px;
.drag-btn {
padding: 0;
border-radius: 10px;
block-size: 48px;
inline-size: 100%;
min-inline-size: 0;
.btn-content {
inline-size: 100%;
}
}
}
}
@media (width <= 600px) {
.component-card {
padding: 8px;
}
.component-item {
margin-block-end: 8px;
}
.components-container {
padding: 8px;
}
.sidebar-header {
padding: 12px;
}
.sidebar-footer {
padding: 8px;
.drag-btn {
block-size: 40px;
}
}
}
</style>

View File

@@ -27,6 +27,7 @@ import VueApexCharts from 'vue3-apexcharts'
// 6. 注册自定义组件
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import ScrollToTopBtn from '@/@core/components/ScrollToTopBtn.vue'
import MediaCard from './components/cards/MediaCard.vue'
import PosterCard from './components/cards/PosterCard.vue'
import BackdropCard from './components/cards/BackdropCard.vue'
@@ -36,6 +37,7 @@ import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import CronField from './components/field/CronField.vue'
import PathField from './components/field/PathField.vue'
import HeaderTab from './layouts/components/HeaderTab.vue'
// 7. 样式文件
import '@core/scss/template/libs/vuetify/index.scss'
@@ -82,6 +84,7 @@ initializeApp().then(() => {
.component('VApexChart', VueApexCharts)
.component('VCronVuetify', CronVuetify)
.component('VDialogCloseBtn', DialogCloseBtn)
.component('VScrollToTopBtn', ScrollToTopBtn)
.component('VMediaCard', MediaCard)
.component('VPosterCard', PosterCard)
.component('VBackdropCard', BackdropCard)
@@ -91,6 +94,7 @@ initializeApp().then(() => {
.component('VMediaIdSelector', MediaIdSelector)
.component('VCronField', CronField)
.component('VPathField', PathField)
.component('VHeaderTab', HeaderTab)
// 5. 注册其他插件
app

View File

@@ -39,5 +39,6 @@ function getApiPath(paths: string[] | string) {
</div>
<PersonCardListView v-if="type === 'person'" :apipath="getApiPath(props.paths || '')" :params="route.query" />
<MediaCardListView v-else :apipath="getApiPath(props.paths || '')" :params="route.query" />
<VScrollToTopBtn />
</div>
</template>

View File

@@ -343,34 +343,49 @@ onDeactivated(() => {
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>设置仪表板</VCardTitle>
<VCardTitle>
<VIcon icon="mdi-tune" size="small" class="me-2" />
设置仪表板
</VCardTitle>
<DialogCloseBtn @click="dialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol
<p class="settings-hint">选择您想在页面显示的内容</p>
<div class="settings-grid">
<div
v-for="item in dashboardConfigs"
:key="buildPluginDashboardId(item.id, item.key)"
cols="6"
md="4"
sm="4"
class="setting-item"
:class="{
'enabled': enableConfig[buildPluginDashboardId(item.id, item.key)],
}"
@click="
enableConfig[buildPluginDashboardId(item.id, item.key)] =
!enableConfig[buildPluginDashboardId(item.id, item.key)]
"
>
<VCheckbox
v-model="enableConfig[buildPluginDashboardId(item.id, item.key)]"
:label="item.attrs?.title ?? item.name"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="isElevated" label="自适应组件高度" />
</VCol>
</VRow>
<div class="setting-item-inner">
<div class="setting-check">
<VIcon
:icon="
enableConfig[buildPluginDashboardId(item.id, item.key)] ? 'mdi-check-circle' : 'mdi-circle-outline'
"
:color="enableConfig[buildPluginDashboardId(item.id, item.key)] ? 'primary' : undefined"
size="small"
/>
</div>
<span class="setting-label">{{ item.attrs?.title ?? item.name }}</span>
</div>
</div>
</div>
<p class="mt-3">
<VSwitch v-model="isElevated" label="自适应组件高度" />
</p>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VSpacer />
<VBtn variant="outlined" color="secondary" class="me-4" @click="dialog = false"> 关闭 </VBtn>
<VBtn @click="saveDashboardConfig">
<template #prepend>
<VIcon icon="mdi-content-save" />
@@ -381,3 +396,74 @@ onDeactivated(() => {
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.settings-card-header {
padding-block: 16px;
padding-inline: 20px;
}
.settings-hint {
color: rgba(var(--v-theme-on-surface), 0.7);
font-size: 0.9rem;
margin-block-end: 16px;
}
.settings-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.setting-label {
color: rgba(var(--v-theme-on-surface), 0.8);
font-size: 0.9rem;
transition: color 0.2s ease;
}
.setting-item {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&::before {
position: absolute;
background-color: transparent;
block-size: 100%;
content: '';
inline-size: 4px;
inset-block-start: 0;
inset-inline-start: 0;
transition: background-color 0.3s ease;
}
&:hover {
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-surface-variant), 0.6);
}
&.enabled {
border-color: rgba(var(--v-theme-primary), 0.5);
background-color: rgba(var(--v-theme-primary), 0.05);
.setting-label {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
}
}
.setting-item-inner {
display: flex;
align-items: center;
}
.setting-check {
margin-inline-end: 8px;
}
</style>

View File

@@ -7,6 +7,7 @@ import BangumiView from '@/views/discover/BangumiView.vue'
import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types'
import api from '@/api'
import { or } from '@vueuse/math'
const activeTab = ref('')
@@ -19,9 +20,19 @@ const orderConfig = ref<{ name: string }[]>([])
// 标签页
const discoverTabs = ref<DiscoverSource[]>([])
// 标签页项
const discoverTabItems = computed(() => {
return discoverTabs.value.map(item => ({
title: item.name,
}))
})
// 额外的数据源
const extraDiscoverSources = ref<DiscoverSource[]>([])
// 排序对话框
const orderConfigDialog = ref(false)
// 初始化发现标签
function initDiscoverTabs() {
for (const tab of DiscoverTabs) {
@@ -85,6 +96,7 @@ async function loadOrderConfig() {
// 保存顺序设置
async function saveTabOrder() {
orderConfigDialog.value = false
// 顺序配置
const orderObj = discoverTabs.value.map(item => ({ name: item.name }))
orderConfig.value = orderObj
@@ -106,7 +118,7 @@ onBeforeMount(async () => {
sortSubscribeOrder()
// 选中第一个标签页
if (discoverTabs.value.length > 0) {
activeTab.value = discoverTabs.value[0].mediaid_prefix
activeTab.value = discoverTabs.value[0].name
}
})
@@ -118,39 +130,42 @@ onActivated(async () => {
<template>
<div>
<VTabs v-model="activeTab" show-arrows stacked>
<draggable v-model="discoverTabs" handle=".tab-move" item-key="tab" tag="div" @end="saveTabOrder">
<template #item="{ element }">
<VTab :key="element.mediaid_prefix" :value="element.mediaid_prefix" class="px-10 rounded-t-lg">
<span class="tab-move">{{ element.name }}</span>
</VTab>
</template>
</draggable>
</VTabs>
<VHeaderTab :items="discoverTabItems" v-model="activeTab">
<template #append>
<VBtn
icon="mdi-order-alphabetical-ascending"
variant="text"
color="primary"
size="default"
class="settings-icon-button"
@click="orderConfigDialog = true"
/>
</template>
</VHeaderTab>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="themoviedb">
<VWindowItem value="TheMovieDb">
<transition name="fade-slide" appear>
<div>
<TheMovieDbView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="douban">
<VWindowItem value="豆瓣">
<transition name="fade-slide" appear>
<div>
<DoubanView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="bangumi">
<VWindowItem value="Bangumi">
<transition name="fade-slide" appear>
<div>
<BangumiView />
</div>
</transition>
</VWindowItem>
<VWindowItem v-for="item in extraDiscoverSources" :key="item.mediaid_prefix" :value="item.mediaid_prefix">
<VWindowItem v-for="item in extraDiscoverSources" :key="item.mediaid_prefix" :value="item.name">
<transition name="fade-slide" appear>
<div>
<ExtraSourceView :source="item" />
@@ -158,5 +173,119 @@ onActivated(async () => {
</transition>
</VWindowItem>
</VWindow>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="orderConfigDialog" v-model="orderConfigDialog" max-width="35rem" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-order-alphabetical-ascending" size="small" class="me-2" />
设置标签顺序
</VCardTitle>
<DialogCloseBtn @click="orderConfigDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<p class="settings-hint">拖动对标签页进行排序</p>
<draggable
v-model="discoverTabs"
handle=".cursor-move"
item-key="mediaid_prefix"
tag="div"
:component-data="{ 'class': 'settings-grid' }"
>
<template #item="{ element }">
<div class="setting-item enabled">
<div class="setting-item-inner cursor-move text-center">
<span class="setting-label">{{ element.name }}</span>
</div>
</div>
</template>
</draggable>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VSpacer />
<VBtn @click="saveTabOrder">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardText>
</VCard>
</VDialog>
<!-- 快速滚动到顶部按钮 -->
<VScrollToTopBtn />
</div>
</template>
<style lang="scss" scoped>
.settings-card-header {
padding-block: 16px;
padding-inline: 20px;
}
.settings-hint {
color: rgba(var(--v-theme-on-surface), 0.7);
font-size: 0.9rem;
margin-block-end: 16px;
}
.settings-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.setting-label {
color: rgba(var(--v-theme-on-surface), 0.8);
font-size: 0.9rem;
transition: color 0.2s ease;
}
.setting-item {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&::before {
position: absolute;
background-color: transparent;
block-size: 100%;
content: '';
inline-size: 4px;
inset-block-start: 0;
inset-inline-start: 0;
transition: background-color 0.3s ease;
}
&:hover {
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-surface-variant), 0.6);
}
&.enabled {
border-color: rgba(var(--v-theme-primary), 0.5);
background-color: rgba(var(--v-theme-primary), 0.05);
.setting-label {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
}
}
.setting-item-inner {
display: flex;
align-items: center;
}
.setting-check {
margin-inline-end: 8px;
}
</style>

View File

@@ -2,7 +2,6 @@
import api from '@/api'
import { DownloaderConf } from '@/api/types'
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
import router from '@/router'
import NoDataFound from '@/components/NoDataFound.vue'
const route = useRoute()
@@ -11,6 +10,13 @@ const activeTab = ref(route.query.tab)
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 下载器字典
const downloaderItems = computed(() => {
return downloaders.value.map(item => ({
title: item.name,
}))
})
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
@@ -22,10 +28,6 @@ async function loadDownloaderSetting() {
}
}
function jumpTab(tab: string) {
router.push('/subscribe/movie?tab=' + tab)
}
onMounted(async () => {
await loadDownloaderSetting()
})
@@ -37,12 +39,7 @@ onActivated(async () => {
<template>
<div v-if="downloaders.length > 0">
<VTabs v-model="activeTab" show-arrows stacked>
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)" class="px-10 rounded-t-lg">
{{ item.name }}
</VTab>
</VTabs>
<VHeaderTab :items="downloaderItems" v-model="activeTab" />
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem v-for="item in downloaders" :value="item.name">
<transition name="fade-slide" appear>

View File

@@ -2,112 +2,97 @@
import api from '@/api'
import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
import { useDisplay } from 'vuetify'
// APP
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 当前选择的分类
const currentCategory = ref('电影')
const currentCategory = ref('全部')
// 定义分类类型
type CategoryType = '电影' | '电视剧' | '动漫' | '榜单'
type CategoryMap = Record<CategoryType, Array<{apipath: string; linkurl: string; title: string}>>
// 预处理的分类视图数据
const categoryViewsMap = reactive<CategoryMap>({
电影: [],
电视剧: [],
动漫: [],
榜单: []
})
// 按分类过滤视图的映射
const getCategoryForView = (title: string): CategoryType => {
if (title.includes('电影') || title.includes('热映') || (title.includes('TOP250') && !title.includes('剧集'))) {
return '电影'
} else if (title.includes('电视剧') || (title.includes('剧集') && !title.includes('动漫'))) {
return '电视剧'
} else if (title.includes('动漫') || title.includes('Bangumi')) {
return '动漫'
} else if (title.includes('TOP') || title.includes('榜') || title.includes('趋势')) {
return '榜单'
}
return '电影' // 默认分类
}
const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>([
const viewList = reactive<{ apipath: string; linkurl: string; title: string; type: string }[]>([
{
apipath: 'recommend/tmdb_trending',
linkurl: '/browse/recommend/tmdb_trending?title=流行趋势',
title: '流行趋势',
type: '榜单',
},
{
apipath: 'recommend/douban_showing',
linkurl: '/browse/recommend/douban_showing?title=正在热映',
title: '正在热映',
type: '电影',
},
{
apipath: 'recommend/bangumi_calendar',
linkurl: '/browse/recommend/bangumi_calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送',
type: '动漫',
},
{
apipath: 'recommend/tmdb_movies',
linkurl: '/browse/recommend/tmdb_movies?title=TMDB热门电影',
title: 'TMDB热门电影',
type: '电影',
},
{
apipath: 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧',
title: 'TMDB热门电视剧',
type: '电视剧',
},
{
apipath: 'recommend/douban_movie_hot',
linkurl: '/browse/recommend/douban_movie_hot?title=豆瓣热门电影',
title: '豆瓣热门电影',
type: '电影',
},
{
apipath: 'recommend/douban_tv_hot',
linkurl: '/browse/recommend/douban_tv_hot?title=豆瓣热门电视剧',
title: '豆瓣热门电视剧',
type: '电视剧',
},
{
apipath: 'recommend/douban_tv_animation',
linkurl: '/browse/recommend/douban_tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫',
type: '动漫',
},
{
apipath: 'recommend/douban_movies',
linkurl: '/browse/recommend/douban_movies?title=豆瓣最新电影',
title: '豆瓣最新电影',
type: '电影',
},
{
apipath: 'recommend/douban_tvs',
linkurl: '/browse/recommend/douban_tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧',
type: '电视剧',
},
{
apipath: 'recommend/douban_movie_top250',
linkurl: '/browse/recommend/douban_movie_top250?title=电影TOP250',
title: '豆瓣电影TOP250',
type: '榜单',
},
{
apipath: 'recommend/douban_tv_weekly_chinese',
linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=豆瓣国产剧集榜',
title: '豆瓣国产剧集榜',
type: '榜单',
},
{
apipath: 'recommend/douban_tv_weekly_global',
linkurl: '/browse/recommend/douban_tv_weekly_global?title=豆瓣全球剧集榜',
title: '豆瓣全球剧集榜',
type: '榜单',
},
])
// 计算当前分类下显示的视图
const filteredViews = computed(() => {
return categoryViewsMap[currentCategory.value as CategoryType]
if (currentCategory.value === '全部') {
return viewList.filter(item => enableConfig.value[item.title])
}
return viewList.filter(item => enableConfig.value[item.title] && item.type === currentCategory.value)
})
// 榜单启用配置, 以title为key
@@ -121,21 +106,6 @@ const dialog = ref(false)
// 额外的数据源
const extraRecommendSources = ref<RecommendSource[]>([])
// 分类视图
function updateCategoryViews() {
// 清空所有分类
(Object.keys(categoryViewsMap) as CategoryType[]).forEach(category => {
categoryViewsMap[category] = []
})
// 先把所有启用的视图按照分类归类
const enabledViews = viewList.filter(item => enableConfig.value[item.title])
enabledViews.forEach(view => {
const category = getCategoryForView(view.title)
categoryViewsMap[category].push(view)
})
}
// 加载额外的发现数据源
async function loadExtraRecommendSources() {
try {
@@ -146,10 +116,9 @@ async function loadExtraRecommendSources() {
apipath: source.api_path,
linkurl: `/browse/recommend/${source.api_path}?title=${source.name}`,
title: source.name,
type: source.type,
})),
)
// 添加新视图后更新分类
updateCategoryViews()
}
} catch (error) {
console.log(error)
@@ -169,8 +138,6 @@ async function loadConfig() {
localStorage.setItem('MP_RECOMMEND', JSON.stringify(response.data.value))
}
}
// 配置加载后更新分类
updateCategoryViews()
}
// 设置项目
@@ -186,22 +153,36 @@ async function saveConfig() {
console.error(error)
}
dialog.value = false
// 保存后更新分类
updateCategoryViews()
}
const scrollToTop = () => {
window.scrollTo({top: 0, behavior: 'smooth'})
}
// 标签图标映射
const categoryIcons: Record<CategoryType, string> = {
电影: 'mdi-movie',
电视剧: 'mdi-television-classic',
动漫: 'mdi-animation',
榜单: 'mdi-trophy'
}
const categoryItems: Record<string, string>[] = [
{
title: '全部',
icon: 'mdi-filmstrip-box-multiple',
key: 'all',
},
{
title: '电影',
icon: 'mdi-movie',
key: 'movie',
},
{
title: '电视剧',
icon: 'mdi-television-classic',
key: 'tv',
},
{
title: '动漫',
icon: 'mdi-animation',
key: 'anime',
},
{
title: '榜单',
icon: 'mdi-trophy',
key: 'rank',
},
]
onBeforeMount(async () => {
await loadConfig()
@@ -214,108 +195,64 @@ onMounted(async () => {
onActivated(async () => {
loadExtraRecommendSources()
})
// 监听分类变更,平滑过渡
watch(currentCategory, () => {
// 当分类变更时,应用渐变动画
const contentGroups = document.querySelectorAll('.content-group')
contentGroups.forEach(group => {
group.classList.add('fade-transition')
})
})
</script>
<template>
<div class="mp-recommend">
<!-- 页面顶部控制栏 -->
<div class="recommend-header">
<div class="header-tabs">
<div
v-for="(category, idx) in ['电影', '电视剧', '动漫', '榜单']"
:key="idx"
class="header-tab"
:class="{ 'active': currentCategory === category }"
@click="currentCategory = category"
>
<VIcon
:icon="categoryIcons[category as CategoryType]"
size="small"
class="header-tab-icon"
/>
<span>{{ category }}</span>
</div>
</div>
<button
class="tune-button"
@click="dialog = true"
>
<div class="tune-icon">
<span></span>
<span></span>
<span></span>
</div>
<span class="tune-text">显示设置</span>
</button>
</div>
<VHeaderTab :items="categoryItems" v-model="currentCategory">
<template #append>
<VBtn
icon="mdi-tune"
variant="text"
color="primary"
size="default"
class="settings-icon-button"
@click="dialog = true"
/>
</template>
</VHeaderTab>
<!-- 滚动内容区域 -->
<div class="recommend-content">
<TransitionGroup name="fade">
<MediaCardSlideView
v-for="item in filteredViews"
:key="item.title"
v-bind="item"
class="content-group"
/>
<MediaCardSlideView v-for="item in filteredViews" :key="item.title" v-bind="item" class="content-group" />
</TransitionGroup>
<div v-if="filteredViews.length === 0" class="empty-category">
<VIcon icon="mdi-alert-circle-outline" size="large" class="empty-icon" />
<p class="empty-text">当前分类下没有可显示的内容</p>
<VBtn
color="primary"
variant="tonal"
size="small"
@click="dialog = true"
>
设置显示内容
</VBtn>
<VBtn color="primary" variant="tonal" size="small" @click="dialog = true"> 设置显示内容 </VBtn>
</div>
</div>
<!-- 设置面板 -->
<VDialog v-model="dialog" width="500" class="settings-dialog" scrollable>
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable>
<VCard class="settings-card">
<VCardItem class="settings-card-header">
<VCardTitle>
<VIcon icon="mdi-tune" size="small" class="me-2" />
自定义内容
</VCardTitle>
<template #append>
<VBtn icon="mdi-close" variant="text" @click="dialog = false" />
</template>
<DialogCloseBtn @click="dialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<p class="settings-hint">选择您想在页面显示的内容</p>
<div class="settings-grid">
<div
v-for="(item, index) in viewList"
:key="index"
class="setting-item"
:class="{
:class="{
'enabled': enableConfig[item.title],
[getCategoryForView(item.title)]: true
[item.type]: true,
}"
@click="enableConfig[item.title] = !enableConfig[item.title]"
>
<div class="setting-item-inner">
<div class="setting-check">
<VIcon
<VIcon
:icon="enableConfig[item.title] ? 'mdi-check-circle' : 'mdi-circle-outline'"
:color="enableConfig[item.title] ? 'primary' : undefined"
size="small"
@@ -326,46 +263,27 @@ watch(currentCategory, () => {
</div>
</div>
</VCardText>
<VDivider />
<VCardActions>
<VBtn
variant="text"
@click="Object.keys(enableConfig).forEach(key => enableConfig[key] = true)"
>
<VCardActions class="pt-5">
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
全选
</VBtn>
<VBtn
variant="text"
@click="Object.keys(enableConfig).forEach(key => enableConfig[key] = false)"
>
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = false))">
全不选
</VBtn>
<VSpacer />
<VBtn variant="text" @click="dialog = false">取消</VBtn>
<VBtn
color="primary"
variant="tonal"
@click="saveConfig"
>
保存设置
<VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 快速滚动到顶部按钮 -->
<div class="global-action-buttons">
<button
class="global-action-button"
@click="scrollToTop"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 14L12 9L17 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<VScrollToTopBtn />
</div>
</template>
@@ -373,97 +291,27 @@ watch(currentCategory, () => {
.mp-recommend {
position: relative;
padding: 0;
max-width: 100%;
}
.recommend-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin-bottom: 16px;
background-color: rgba(var(--v-theme-primary), 0.02);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(var(--v-theme-primary), 0.1);
}
.header-tabs {
display: flex;
gap: 12px;
overflow-x: auto;
scrollbar-width: none;
padding: 4px;
&::-webkit-scrollbar {
display: none;
}
}
.header-tab {
display: flex;
align-items: center;
padding: 6px 14px;
border-radius: 20px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s ease;
background-color: transparent;
position: relative;
color: rgba(var(--v-theme-on-background), 0.7);
&::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 70%;
height: 3px;
background-color: rgb(var(--v-theme-primary));
border-radius: 3px;
transition: transform 0.2s ease;
}
&.active {
color: rgb(var(--v-theme-primary));
&::after {
transform: translateX(-50%) scaleX(1);
}
.header-tab-icon {
color: rgb(var(--v-theme-primary));
}
}
&:hover:not(.active) {
color: rgba(var(--v-theme-on-background), 1);
background-color: rgba(var(--v-theme-primary), 0.05);
}
}
.header-tab-icon {
margin-right: 6px;
transition: color 0.2s ease;
color: rgba(var(--v-theme-on-background), 0.6);
}
.settings-btn {
min-width: auto;
width: 48px;
height: 48px;
border-radius: 50%;
max-inline-size: 100%;
}
.recommend-content {
padding: 0 8px;
min-height: 300px;
padding-block: 0;
}
/* Fade transition for content groups */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.content-group {
margin-block-end: 24px;
transition: opacity 0.3s ease;
}
.empty-category {
@@ -471,238 +319,107 @@ watch(currentCategory, () => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
background-color: rgba(var(--v-theme-surface), 0.5);
border-radius: 12px;
margin: 20px 0;
border: 1px dashed rgba(var(--v-theme-on-surface), 0.1);
padding: 40px;
color: rgba(var(--v-theme-on-surface), 0.6);
text-align: center;
}
.empty-icon {
margin-block-end: 16px;
opacity: 0.5;
margin-bottom: 12px;
}
.empty-text {
color: rgba(var(--v-theme-on-surface), 0.6);
margin-bottom: 16px;
}
.content-group {
margin-bottom: 24px;
}
.settings-card {
border-radius: 12px;
overflow: hidden;
font-size: 1rem;
margin-block-end: 16px;
}
/* Settings Dialog Styles */
.settings-card-header {
background-color: rgba(var(--v-theme-primary), 0.03);
padding-block: 16px;
padding-inline: 20px;
}
.settings-hint {
color: rgba(var(--v-theme-on-surface), 0.7);
font-size: 0.9rem;
color: rgba(var(--v-theme-on-surface), 0.6);
margin-bottom: 16px;
margin-block-end: 16px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.setting-label {
color: rgba(var(--v-theme-on-surface), 0.8);
font-size: 0.9rem;
transition: color 0.2s ease;
}
.setting-item {
position: relative;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
padding-block: 10px;
padding-inline: 12px;
transition: all 0.2s ease;
&::before {
position: absolute;
background-color: transparent;
block-size: 100%;
content: '';
inline-size: 4px;
inset-block-start: 0;
inset-inline-start: 0;
transition: background-color 0.3s ease;
}
&.电影::before {
background-color: #4caf50;
} // Green
&.电视剧::before {
background-color: #2196f3;
} // Blue
&.动漫::before {
background-color: #ff9800;
} // Orange
&.榜单::before {
background-color: #9c27b0;
} // Purple
&:hover {
border-color: rgba(var(--v-theme-on-surface), 0.15);
background-color: rgba(var(--v-theme-surface-variant), 0.6);
}
&.enabled {
.setting-item-inner {
background-color: rgba(var(--v-theme-primary), 0.08);
border-color: rgba(var(--v-theme-primary), 0.2);
border-color: rgba(var(--v-theme-primary), 0.5);
background-color: rgba(var(--v-theme-primary), 0.05);
.setting-label {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
}
&.电影 .setting-item-inner {
border-left: 3px solid #3b82f6;
}
&.电视剧 .setting-item-inner {
border-left: 3px solid #6366f1;
}
&.动漫 .setting-item-inner {
border-left: 3px solid #a855f7;
}
&.榜单 .setting-item-inner {
border-left: 3px solid #f59e0b;
}
}
.setting-item-inner {
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
background-color: rgba(var(--v-theme-surface), 1);
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(var(--v-theme-on-surface), 0.08);
}
}
.setting-check {
margin-right: 8px;
}
.setting-label {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-action-buttons {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 16px;
}
.global-action-button {
width: 44px;
height: 44px;
background-color: rgba(var(--v-theme-surface), 0.8);
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 50%;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.12);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
color: rgb(var(--v-theme-on-surface));
opacity: 0.7;
&:hover {
background-color: rgba(var(--v-theme-surface), 0.95);
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
opacity: 1;
color: rgb(var(--v-theme-primary));
}
svg {
transition: all 0.3s ease;
width: 20px;
height: 20px;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease, transform 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(20px);
}
.fade-move {
transition: transform 0.5s ease;
}
.fade-transition {
animation: fadeInOut 0.5s ease;
}
@keyframes fadeInOut {
0% {
opacity: 0.5;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
margin-inline-end: 8px;
}
/* Remove old tune button styles if they exist */
.tune-button {
display: flex;
align-items: center;
background: rgba(var(--v-theme-primary), 0.1);
border: none;
border-radius: 30px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.3s ease;
color: rgb(var(--v-theme-primary));
&:hover {
background: rgba(var(--v-theme-primary), 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(var(--v-theme-primary), 0.2);
}
.tune-icon {
display: flex;
flex-direction: column;
width: 16px;
height: 16px;
justify-content: space-between;
margin-right: 8px;
span {
display: block;
height: 2px;
background-color: rgb(var(--v-theme-primary));
border-radius: 2px;
transition: all 0.3s ease;
&:nth-child(1) {
width: 60%;
}
&:nth-child(2) {
width: 80%;
}
&:nth-child(3) {
width: 40%;
}
}
}
.tune-text {
font-weight: 500;
font-size: 0.9rem;
}
&:hover .tune-icon span {
&:nth-child(1) {
width: 100%;
}
&:nth-child(2) {
width: 60%;
}
&:nth-child(3) {
width: 80%;
}
}
display: none; // Hide the old button definitively
}
</style>

View File

@@ -183,18 +183,16 @@ onUnmounted(() => {
<!-- 加载进度条 -->
<VFadeTransition>
<div v-if="progressValue > 0" class="search-progress-container">
<div class="search-progress-card">
<VCard class="search-progress-card">
<div class="progress-header">
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
<span class="progress-title">{{ progressText }}</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-wrapper">
<div class="progress-bar" :style="{ width: `${progressValue}%` }"></div>
</div>
<VProgressLinear color="primary" rounded :model-value="progressValue" />
<div class="progress-percentage">{{ Math.ceil(progressValue) }}%</div>
</div>
</div>
</VCard>
</div>
</VFadeTransition>
@@ -270,42 +268,42 @@ onUnmounted(() => {
<div class="initial-loading-text">搜索中</div>
</div>
</div>
<!-- 滚动到顶部按钮 -->
<VScrollToTopBtn />
</div>
</template>
<style scoped>
.search-progress-container {
position: fixed;
top: env(safe-area-inset-top);
left: 0;
right: 0;
z-index: 100;
display: flex;
justify-content: center;
padding-top: 4rem;
inset-block-start: env(safe-area-inset-top);
inset-inline: 0;
padding-block-start: 4rem;
}
.search-progress-card {
max-width: 400px;
width: 90%;
background-color: rgb(var(--v-theme-surface));
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(var(--v-theme-primary), 0.1);
border-radius: 12px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 10%);
inline-size: 90%;
max-inline-size: 400px;
}
.progress-header {
display: flex;
align-items: center;
margin-bottom: 12px;
margin-block-end: 12px;
}
.progress-title {
color: rgb(var(--v-theme-on-surface));
font-size: 0.9rem;
font-weight: 500;
color: rgb(var(--v-theme-on-surface));
}
.progress-bar-container {
@@ -314,40 +312,20 @@ onUnmounted(() => {
gap: 12px;
}
.progress-bar-wrapper {
flex: 1;
height: 4px;
background-color: rgba(var(--v-theme-on-surface), 0.08);
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(
90deg,
rgb(var(--v-theme-primary)) 0%,
rgb(var(--v-theme-primary)) 70%,
rgba(var(--v-theme-primary), 0.8) 100%
);
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-percentage {
color: rgb(var(--v-theme-primary));
font-size: 0.8rem;
font-weight: 600;
color: rgb(var(--v-theme-primary));
min-width: 36px;
text-align: right;
min-inline-size: 36px;
text-align: end;
}
/* 精简标题栏样式 */
.search-header {
padding: 12px 16px;
background-color: rgb(var(--v-theme-surface));
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 5%);
padding-block: 12px;
padding-inline: 16px;
}
.search-info-container {
@@ -374,27 +352,26 @@ onUnmounted(() => {
.view-toggle-buttons {
display: flex;
background-color: rgba(var(--v-theme-surface-variant), 0.1);
border-radius: 8px;
padding: 4px;
border-radius: 8px;
background-color: rgba(var(--v-theme-surface-variant), 0.1);
}
.view-toggle-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 36px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 6px;
background: transparent;
block-size: 36px;
cursor: pointer;
inline-size: 40px;
transition: all 0.2s ease;
}
.view-toggle-btn.active {
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 10%);
}
.view-toggle-btn:hover:not(.active) {
@@ -404,16 +381,13 @@ onUnmounted(() => {
/* 视图切换加载状态 */
.view-changing-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(var(--v-theme-background), 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
background-color: rgba(var(--v-theme-background), 0.7);
inset: 0;
}
.view-changing-content {
@@ -429,11 +403,11 @@ onUnmounted(() => {
}
.pulse-circle {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgb(var(--v-theme-primary));
animation: pulse 1.2s ease-in-out infinite;
background-color: rgb(var(--v-theme-primary));
block-size: 12px;
inline-size: 12px;
}
.pulse-circle:nth-child(2) {
@@ -447,28 +421,29 @@ onUnmounted(() => {
@keyframes pulse {
0%,
100% {
transform: scale(0.8);
opacity: 0.5;
transform: scale(0.8);
}
50% {
transform: scale(1.2);
opacity: 1;
transform: scale(1.2);
}
}
.view-changing-text {
color: rgb(var(--v-theme-primary));
font-size: 0.9rem;
font-weight: 500;
color: rgb(var(--v-theme-primary));
letter-spacing: 1px;
}
/* 初始的加载状态 */
.initial-loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 50vh;
justify-content: center;
min-block-size: 50vh;
}
.initial-loading-content {
@@ -481,16 +456,16 @@ onUnmounted(() => {
.wave-loader {
display: flex;
align-items: center;
block-size: 40px;
gap: 6px;
height: 40px;
}
.wave-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgb(var(--v-theme-primary));
animation: wave 1.5s ease-in-out infinite;
background-color: rgb(var(--v-theme-primary));
block-size: 8px;
inline-size: 8px;
}
.wave-dot:nth-child(1) {
@@ -514,26 +489,28 @@ onUnmounted(() => {
100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
.initial-loading-text {
color: rgb(var(--v-theme-primary));
font-size: 0.9rem;
font-weight: 500;
color: rgb(var(--v-theme-primary));
letter-spacing: 1px;
}
.search-results-container {
min-height: 50vh;
position: relative;
min-block-size: 50vh;
}
@media (max-width: 600px) {
@media (width <= 600px) {
.search-header {
padding: 8px 12px;
padding-block: 8px;
padding-inline: 12px;
}
.search-title {
@@ -542,17 +519,17 @@ onUnmounted(() => {
}
.search-info-container {
overflow: hidden;
flex: 1;
gap: 8px;
min-width: 0;
overflow: hidden;
min-inline-size: 0;
}
.search-tags {
overflow-x: auto;
flex-wrap: nowrap;
margin-inline-end: 8px;
overflow-x: auto;
scrollbar-width: none;
margin-right: 8px;
}
.search-tags::-webkit-scrollbar {
@@ -568,8 +545,8 @@ onUnmounted(() => {
}
.view-toggle-btn {
width: 36px;
height: 32px;
block-size: 32px;
inline-size: 36px;
}
}
</style>

View File

@@ -2,8 +2,9 @@
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import { SubscribeMovieTabs, SubscribeTvTabs } from '@/router/menu'
import router from '@/router'
const route = useRoute()
@@ -11,52 +12,41 @@ const subType = route.meta.subType?.toString()
const subId = ref(route.query.id as string)
const activeTab = ref(route.query.tab)
function jumpTab(tab: string) {
router.push('/subscribe/movie?tab=' + tab)
}
// 弹窗
const subscribeEditDialog = ref(false)
</script>
<template>
<div>
<VTabs v-model="activeTab" show-arrows stacked>
<VTab
v-if="subType == '电影'"
v-for="item in SubscribeMovieTabs"
:value="item.tab"
@to="jumpTab(item.tab)"
class="px-10 rounded-t-lg"
>
<VIcon size="x-large" start :icon="item.icon" />
{{ item.title }}
</VTab>
<VTab
v-if="subType == '电视剧'"
v-for="item in SubscribeTvTabs"
:value="item.tab"
@to="jumpTab(item.tab)"
class="px-10 rounded-t-lg"
>
<VIcon size="x-large" start :icon="item.icon" />
{{ item.title }}
</VTab>
</VTabs>
<VHeaderTab :items="subType == '电影' ? SubscribeMovieTabs : SubscribeTvTabs" v-model="activeTab">
<template #append>
<VBtn
icon="mdi-clipboard-edit-outline"
variant="text"
color="primary"
size="default"
class="settings-icon-button"
@click="subscribeEditDialog = true"
/>
</template>
</VHeaderTab>
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
<VWindowItem value="mysub">
<VWindowItem value="我的订阅">
<transition name="fade-slide" appear>
<div class="mt-4">
<SubscribeListView :type="subType" :subid="subId" />
</div>
</transition>
</VWindowItem>
<VWindowItem value="popular">
<VWindowItem value="热门订阅">
<transition name="fade-slide" appear>
<div>
<SubscribePopularView :type="subType" />
</div>
</transition>
</VWindowItem>
<VWindowItem value="share">
<VWindowItem value="订阅分享">
<transition name="fade-slide" appear>
<div>
<SubscribeShareView />
@@ -64,5 +54,15 @@ function jumpTab(tab: string) {
</transition>
</VWindowItem>
</VWindow>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:default="true"
:type="subType"
@save="subscribeEditDialog = false"
@close="subscribeEditDialog = false"
/>
</div>
</template>

View File

@@ -8,7 +8,7 @@ html.v-overlay-scroll-blocked {
position: fixed;
}
html.v-overlay-scroll-blocked body {
html {
--v-body-scroll-y: 0px !important;
}
@@ -134,7 +134,6 @@ html.v-overlay-scroll-blocked body {
.backdrop-blur {
--tw-backdrop-blur: blur(8px)!important;
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
}
@@ -262,10 +261,25 @@ html.v-overlay-scroll-blocked body {
inset: 0;
}
.v-list-item {
margin-block: 2px !important;
margin-inline: 0 !important;
transition: background-color 0.15s ease;
.v-overlay__content .v-list{
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.9) !important;
}
.v-overlay__content .v-card:not(.bg-primary){
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: rgb(var(--v-theme-surface), 0.95) !important;
.v-list, .v-table {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: none;
backdrop-filter: none;
background-color: transparent !important;
}
}
.v-list-item:hover {
@@ -281,23 +295,10 @@ html.v-overlay-scroll-blocked body {
}
.v-overlay__content {
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
transition: opacity 0.2s ease !important;
}
.v-overlay__content .v-list{
padding: 0;
margin: 0 !important;
background-color: rgb(var(--v-theme-surface)) !important;
}
.v-overlay__content .v-card:not(.bg-primary){
background-color: rgb(var(--v-theme-surface)) !important;
.v-list, .v-table {
background-color: transparent !important;
}
}
.v-menu > .v-overlay__content {
overflow: hidden;
}
@@ -313,3 +314,8 @@ html.v-overlay-scroll-blocked body {
.v-bottom-sheet > .v-bottom-sheet__content.v-overlay__content > .v-card {
padding-block-end: env(safe-area-inset-bottom);
}
.settings-icon-button {
flex-shrink: 0;
min-inline-size: auto;
}

View File

@@ -21,7 +21,7 @@ const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 当前标签
const activeTab = ref(route.query.tab)
const activeTab = ref('我的插件')
// 插件ID参数
const pluginId = ref(route.query.id)
@@ -397,16 +397,22 @@ onMounted(async () => {
<template>
<div>
<VTabs v-model="activeTab" show-arrows stacked>
<VTab v-for="item in PluginTabs" :value="item.tab" class="px-10 rounded-t-lg">
<VIcon size="x-large" start :icon="item.icon" />
{{ item.title }}
</VTab>
</VTabs>
<VHeaderTab :items="PluginTabs" v-model="activeTab">
<template #append>
<VBtn
icon="mdi-store-cog"
variant="text"
color="primary"
size="default"
class="settings-icon-button"
@click="MarketSettingDialog = true"
/>
</template>
</VHeaderTab>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 我的插件 -->
<VWindowItem value="installed">
<VWindowItem value="我的插件">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
@@ -440,7 +446,7 @@ onMounted(async () => {
</transition>
</VWindowItem>
<!-- 插件市场 -->
<VWindowItem value="market">
<VWindowItem value="插件市场">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
@@ -519,18 +525,6 @@ onMounted(async () => {
app
appear
@click="SearchDialog = true"
:class="appMode ? 'mb-28' : 'mb-16'"
/>
<!-- 插件市场设置图标 -->
<VFab
icon="mdi-store-cog"
color="warning"
location="bottom"
size="x-large"
fixed
app
appear
@click="MarketSettingDialog = true"
:class="{ 'mb-12': appMode }"
/>
</div>

View File

@@ -426,7 +426,7 @@ onMounted(fetchData)
</script>
<template>
<VCard class="bg-transparent">
<VCard>
<VCardItem>
<VCardTitle>
<VRow>
@@ -442,22 +442,19 @@ onMounted(fetchData)
label="搜索整理记录"
prepend-inner-icon="mdi-magnify"
variant="solo-filled"
max-width="25rem"
single-line
hide-details
flat
rounded
rounded="pill"
clearable
/>
</VCol>
<VCol cols="4" md="6" class="text-end">
<VBtn
color="primary"
prepend-icon="mdi-tray-full"
append-icon="mdi-dots-horizontal"
@click="transferQueueDialog = true"
>
<span v-if="display.mdAndUp.value" class="ms-2">整理队列</span>
</VBtn>
<VBtnGroup variant="outlined" divided rounded>
<VBtn icon="mdi-timer-sand-paused" @click="transferQueueDialog = true" />
<VBtn :icon="group ? 'mdi-format-list-bulleted' : 'mdi-format-list-group'" @click="group = !group" />
</VBtnGroup>
</VCol>
</VRow>
</VCardTitle>
@@ -548,7 +545,6 @@ onMounted(fetchData)
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
@@ -635,7 +631,6 @@ onMounted(fetchData)
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
@@ -650,9 +645,10 @@ onMounted(fetchData)
</template>
<template #no-data> 没有数据 </template>
</VDataTableVirtual>
<VDivider />
<div class="flex items-center justify-between">
<div class="w-auto">
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" variant="plain" flat />
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" variant="tonal" flat />
</div>
<div class="w-auto text-sm">{{ pageTip.begin }} - {{ pageTip.end }} / {{ totalItems }}</div>
<VPagination
@@ -678,10 +674,10 @@ onMounted(fetchData)
app
appear
@click="removeHistoryBatch"
:class="{ 'mb-12': appMode }"
:class="appMode ? 'mb-28' : 'mb-16'"
/>
<VFab
:class="appMode ? 'mb-28' : 'mb-16'"
:class="appMode ? 'mb-44' : 'mb-32'"
icon="mdi-redo-variant"
location="bottom"
size="x-large"
@@ -691,19 +687,6 @@ onMounted(fetchData)
@click="retransferBatch"
/>
</div>
<div v-else-if="isRefreshed">
<VFab
:icon="group ? 'mdi-format-list-bulleted' : 'mdi-format-list-group'"
color="primary"
location="bottom"
size="x-large"
fixed
app
appear
@click="group = !group"
:class="{ 'mb-12': appMode }"
/>
</div>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center rounded-t">
@@ -742,4 +725,8 @@ onMounted(fetchData)
.v-table th {
white-space: nowrap;
}
.v-table__wrapper {
border-radius: 0;
}
</style>

View File

@@ -208,22 +208,22 @@ onMounted(() => {
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addNotification('wechat')">
<VListItem @click="addNotification('wechat')">
<VListItemTitle>微信</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addNotification('telegram')">
<VListItem @click="addNotification('telegram')">
<VListItemTitle>Telegram</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addNotification('slack')">
<VListItem @click="addNotification('slack')">
<VListItemTitle>Slack</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addNotification('synologychat')">
<VListItem @click="addNotification('synologychat')">
<VListItemTitle>SynologyChat</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addNotification('vocechat')">
<VListItem @click="addNotification('vocechat')">
<VListItemTitle>VoceChat</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addNotification('webpush')">
<VListItem @click="addNotification('webpush')">
<VListItemTitle>WebPush</VListItemTitle>
</VListItem>
</VList>

View File

@@ -481,10 +481,10 @@ onDeactivated(() => {
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addDownloader('qbittorrent')">
<VListItem @click="addDownloader('qbittorrent')">
<VListItemTitle>Qbittorrent</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addDownloader('transmission')">
<VListItem @click="addDownloader('transmission')">
<VListItemTitle>Transmission</VListItemTitle>
</VListItem>
</VList>
@@ -529,16 +529,16 @@ onDeactivated(() => {
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addMediaServer('emby')">
<VListItem @click="addMediaServer('emby')">
<VListItemTitle>Emby</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('jellyfin')">
<VListItem @click="addMediaServer('jellyfin')">
<VListItemTitle>Jellyfin</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('plex')">
<VListItem @click="addMediaServer('plex')">
<VListItemTitle>Plex</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('trimemedia')">
<VListItem @click="addMediaServer('trimemedia')">
<VListItemTitle>飞牛影视</VListItemTitle>
</VListItem>
</VList>

View File

@@ -4,7 +4,6 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import { useUserStore } from '@/stores'
import { useDisplay } from 'vuetify'
@@ -35,9 +34,6 @@ const loading = ref(false)
// 数据列表
const dataList = ref<Subscribe[]>([])
// 弹窗
const subscribeEditDialog = ref(false)
// 历史记录弹窗
const historyDialog = ref(false)
@@ -165,23 +161,12 @@ onActivated(async () => {
/>
<!-- 底部操作按钮 -->
<div v-if="isRefreshed">
<VFab
v-if="userStore.superUser"
icon="mdi-clipboard-edit"
location="bottom"
size="x-large"
fixed
app
appear
@click="subscribeEditDialog = true"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-if="userStore.superUser"
icon="mdi-history"
color="info"
location="bottom"
:class="appMode ? 'mb-28' : 'mb-16'"
:class="{ 'mb-12': appMode }"
size="x-large"
fixed
app
@@ -189,15 +174,6 @@ onActivated(async () => {
@click="historyDialog = true"
/>
</div>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:default="true"
:type="props.type"
@save="subscribeEditDialog = false"
@close="subscribeEditDialog = false"
/>
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"

View File

@@ -380,7 +380,7 @@ function removeFilter(key: string, value: string) {
function loadMore({ done }: { done: any }) {
// 从 dataList 中获取最前面的 20 个元素
const itemsToMove = dataList.splice(0, 20)
const itemsToMove = dataList.splice(0, 20)
displayDataList.value.push(...itemsToMove)
done('ok')
}
@@ -401,11 +401,11 @@ function loadMore({ done }: { done: any }) {
:items="Object.entries(sortTitles).map(([key, title]) => ({ title, value: key }))"
item-title="title"
item-value="value"
variant="plain"
density="compact"
hide-details
class="sort-select"
prepend-icon="mdi-sort"
variant="plain"
></VSelect>
</div>
@@ -520,11 +520,11 @@ function loadMore({ done }: { done: any }) {
:items="Object.entries(sortTitles).map(([key, title]) => ({ title, value: key }))"
item-title="title"
item-value="value"
variant="plain"
density="compact"
hide-details
class="mobile-sort-select"
prepend-icon="mdi-sort"
variant="plain"
></VSelect>
</div>
@@ -623,8 +623,6 @@ function loadMore({ done }: { done: any }) {
.view-header {
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
background-color: rgb(var(--v-theme-surface));
}
.sort-container {
@@ -648,18 +646,6 @@ function loadMore({ done }: { done: any }) {
transform: translateY(-2px);
}
.sort-select {
font-size: 0.9rem;
font-weight: 500;
max-inline-size: 160px;
min-inline-size: 120px;
}
.sort-select :deep(.v-field__input) {
min-block-size: 36px;
padding-block: 5px;
}
.selected-filters {
overflow: hidden;
border-radius: 0 0 12px 12px;
@@ -714,11 +700,6 @@ function loadMore({ done }: { done: any }) {
}
@media (width <= 600px) {
.sort-select {
max-inline-size: 120px;
min-inline-size: 100px;
}
.filter-btn {
font-size: 0.75rem;
}
@@ -730,10 +711,6 @@ function loadMore({ done }: { done: any }) {
padding-inline-end: 0;
}
.sort-select {
inline-size: 100%;
}
.filter-bar {
inline-size: 100%;
margin-block-start: 8px;

View File

@@ -233,22 +233,11 @@ function initOptions(data: Context) {
optionValue(filterOptions.resolution, meta_info?.resource_pix)
}
// 监听数据列表,进行排序
watchEffect(() => {
const list = dataList.value
if (sortField.value === 'default') {
dataList.value = list.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
} else if (sortField.value === 'site') {
dataList.value = list.sort((a, b) => (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''))
} else if (sortField.value === 'size') {
dataList.value = list.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
} else if (sortField.value === 'seeder') {
dataList.value = list.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders)
}
})
// 修改watch监听同时监听排序字段的变化
watch([filterForm, sortField], filterData)
// 计算过滤后的列表
watchEffect(() => {
function filterData() {
// 清空列表
dataList.value = []
displayDataList.value = []
@@ -264,7 +253,7 @@ watchEffect(() => {
})
// 筛选数据
const filteredData: Context[] = []
let filteredData: Context[] = []
// 然后根据过滤条件筛选数据
props.items.forEach(data => {
@@ -288,12 +277,26 @@ watchEffect(() => {
filteredData.push(data)
}
})
// 排序
if (sortField.value === 'default') {
filteredData = filteredData.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
} else if (sortField.value === 'site') {
filteredData = filteredData.sort((a, b) =>
(a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''),
)
} else if (sortField.value === 'size') {
filteredData = filteredData.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
} else if (sortField.value === 'seeder') {
filteredData = filteredData.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders)
}
// 显示前20个
displayDataList.value = filteredData.slice(0, 20)
// 保存剩余数据
dataList.value = filteredData.slice(20)
}
})
}
// 过滤菜单相关
const filterMenuOpen = ref(false)
@@ -350,6 +353,10 @@ function loadMore({ done }: { done: any }) {
displayDataList.value.push(...itemsToMove)
done('ok')
}
onMounted(() => {
filterData()
})
</script>
<template>
@@ -369,11 +376,11 @@ function loadMore({ done }: { done: any }) {
:items="Object.entries(sortTitles).map(([key, title]) => ({ title, value: key }))"
item-title="title"
item-value="value"
variant="plain"
density="compact"
hide-details
class="sort-select"
prepend-icon="mdi-sort"
variant="plain"
>
</VSelect>
<div class="filter-divider"></div>
@@ -486,11 +493,11 @@ function loadMore({ done }: { done: any }) {
:items="Object.entries(sortTitles).map(([key, title]) => ({ title, value: key }))"
item-title="title"
item-value="value"
variant="plain"
density="compact"
hide-details
class="mobile-sort-select"
prepend-icon="mdi-sort"
variant="plain"
></VSelect>
</div>
@@ -614,9 +621,6 @@ function loadMore({ done }: { done: any }) {
.view-header {
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
background-color: rgb(var(--v-theme-surface));
box-shadow: none;
}
.search-count {
@@ -647,12 +651,6 @@ function loadMore({ done }: { done: any }) {
transform: translateY(-2px);
}
.sort-select {
font-size: 0.875rem;
max-inline-size: 120px;
min-inline-size: 100px;
}
.filter-menu-content {
max-block-size: 50vh;
overflow-y: auto;
@@ -704,7 +702,6 @@ function loadMore({ done }: { done: any }) {
padding: 8px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: 12px;
background-color: rgb(var(--v-theme-surface));
}
.resource-list {
@@ -721,11 +718,6 @@ function loadMore({ done }: { done: any }) {
min-block-size: 300px;
}
.mobile-sort-select {
max-inline-size: 130px;
min-inline-size: 110px;
}
.filter-buttons-grid {
display: grid;
gap: 4px;
@@ -756,9 +748,8 @@ function loadMore({ done }: { done: any }) {
text-align: center;
}
@media (width <= 600px) {
.sort-select {
min-inline-size: 100px;
}
.mobile-sort-select {
max-inline-size: 130px;
min-inline-size: 110px;
}
</style>