mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 22:02:39 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b53cd0a09 | ||
|
|
3d7a0d9b0d | ||
|
|
114844ad48 | ||
|
|
f83f709080 | ||
|
|
999a6e7c6e | ||
|
|
2fd4c0b2ea | ||
|
|
16d62642f6 | ||
|
|
96a0ce8c5f | ||
|
|
7703d8157c | ||
|
|
87aa4e902c | ||
|
|
c86f32fab5 | ||
|
|
f85ac34753 | ||
|
|
f58d4fcb7e | ||
|
|
675c32cee3 | ||
|
|
de011b35db | ||
|
|
288a7ebc20 | ||
|
|
d7c3167ecd | ||
|
|
3205ae3ebe | ||
|
|
2ba609fb78 | ||
|
|
7e70b1b7ab | ||
|
|
561bdf4137 | ||
|
|
22a2bb65c8 | ||
|
|
c4f6db9f9f | ||
|
|
e5d2140ea3 | ||
|
|
83e57deec3 | ||
|
|
fc357a03e5 | ||
|
|
f031077fbd | ||
|
|
02de63210d | ||
|
|
98610e3e0d | ||
|
|
bb6cfd9d0e | ||
|
|
57c6d7e8f3 | ||
|
|
6d8b850b15 | ||
|
|
db6c3ea36c | ||
|
|
0ddf7ab070 | ||
|
|
93686bd354 | ||
|
|
89e4a68a03 |
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.3.8",
|
||||
"version": "2.3.9",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
|
||||
85
src/@core/components/ScrollToTopBtn.vue
Normal file
85
src/@core/components/ScrollToTopBtn.vue
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,6 +1265,8 @@ export interface RecommendSource {
|
||||
name: string
|
||||
// 媒体数据源API地址
|
||||
api_path: string
|
||||
// 类型
|
||||
type: string
|
||||
}
|
||||
|
||||
// 站点资源分类
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() }} 次复用
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="新建文件夹">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
198
src/layouts/components/HeaderTab.vue
Normal file
198
src/layouts/components/HeaderTab.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user