fix: make sort mode drag-only across cards

This commit is contained in:
jxxghp
2026-05-09 18:04:10 +08:00
parent 62e0d8e9dc
commit 088db67089
11 changed files with 110 additions and 35 deletions
+17 -3
View File
@@ -273,6 +273,14 @@ function openPluginDetail() {
else showPluginConfig() else showPluginConfig()
} }
function handleCardClick() {
if (props.sortable) {
return
}
openPluginDetail()
}
// 配置完成 // 配置完成
function configDone() { function configDone() {
pluginConfigDialog.value = false pluginConfigDialog.value = false
@@ -438,11 +446,13 @@ watch(
v-bind="hover.props" v-bind="hover.props"
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
@click="openPluginDetail" @click="handleCardClick"
class="flex flex-col h-full" class="flex flex-col h-full"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
'cursor-move': props.sortable,
}" }"
:ripple="!props.sortable"
> >
<div <div
class="flex-grow" class="flex-grow"
@@ -487,7 +497,11 @@ watch(
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" /> <VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
</template> </template>
</VImg> </VImg>
<span v-if="props.sortable" class="overflow-hidden text-ellipsis whitespace-nowrap">
{{ props.plugin?.plugin_author }}
</span>
<a <a
v-else
:href="props.plugin?.author_url" :href="props.plugin?.author_url"
target="_blank" target="_blank"
@click.stop @click.stop
@@ -501,7 +515,7 @@ watch(
<span class="text-sm">{{ formatDownloadCount(props.count) }}</span> <span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
</span> </span>
</div> </div>
<div class="absolute bottom-0 right-0"> <div v-if="!props.sortable" class="absolute bottom-0 right-0">
<IconBtn> <IconBtn>
<VIcon icon="mdi-dots-vertical" /> <VIcon icon="mdi-dots-vertical" />
<VMenu v-model="menuVisible" activator="parent" close-on-content-click> <VMenu v-model="menuVisible" activator="parent" close-on-content-click>
+16 -3
View File
@@ -169,6 +169,14 @@ function openFolder() {
emit('open', props.folderName) emit('open', props.folderName)
} }
function handleCardClick() {
if (props.sortable) {
return
}
openFolder()
}
// 重命名文件夹 // 重命名文件夹
function showRenameDialog() { function showRenameDialog() {
newFolderName.value = props.folderName || '' newFolderName.value = props.folderName || ''
@@ -279,11 +287,12 @@ const dropdownItems = ref([
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
min-height="8.5rem" min-height="8.5rem"
@click="openFolder" @click="handleCardClick"
class="plugin-folder-card h-full" class="plugin-folder-card h-full"
:class="{ :class="{
'plugin-folder-card--mobile': display.mobile, 'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--hover': hover.isHovering, 'plugin-folder-card--hover': hover.isHovering && !props.sortable,
'plugin-folder-card--sortable': props.sortable,
}" }"
> >
<template v-if="backgroundImage" #image> <template v-if="backgroundImage" #image>
@@ -325,7 +334,7 @@ const dropdownItems = ref([
</div> </div>
<!-- 更多菜单按钮 - 右下角 --> <!-- 更多菜单按钮 - 右下角 -->
<div class="absolute top-0 right-0"> <div v-if="!props.sortable" class="absolute top-0 right-0">
<VMenu v-model="menuVisible" location="top end" :close-on-content-click="true"> <VMenu v-model="menuVisible" location="top end" :close-on-content-click="true">
<template #activator="{ props: menuProps }"> <template #activator="{ props: menuProps }">
<IconBtn v-bind="menuProps" @click.stop> <IconBtn v-bind="menuProps" @click.stop>
@@ -495,6 +504,10 @@ const dropdownItems = ref([
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--sortable {
cursor: move;
}
&--hover { &--hover {
transform: translateY(-4px); transform: translateY(-4px);
} }
+1 -1
View File
@@ -113,7 +113,7 @@ function handleDropToFolder(event: DragEvent) {
<!-- 移出文件夹按钮(仅在文件夹内显示) --> <!-- 移出文件夹按钮(仅在文件夹内显示) -->
<VBtn <VBtn
v-if="showRemoveButton" v-if="showRemoveButton && !sortable"
icon="mdi-folder-remove" icon="mdi-folder-remove"
variant="text" variant="text"
color="warning" color="warning"
+55 -16
View File
@@ -123,6 +123,22 @@ function openSitePage() {
window.open(cardProps.site?.url, '_blank') window.open(cardProps.site?.url, '_blank')
} }
function handleCardClick() {
if (cardProps.sortable) {
return
}
handleResourceBrowse()
}
function handleSiteUrlClick() {
if (cardProps.sortable) {
return
}
openSitePage()
}
// 调用API删除站点信息 // 调用API删除站点信息
async function deleteSiteInfo() { async function deleteSiteInfo() {
const isConfirmed = await createConfirm({ const isConfirmed = await createConfirm({
@@ -210,21 +226,24 @@ onMounted(() => {
<template> <template>
<div> <div>
<VCard <VCard
class="site-card relative h-full flex flex-col overflow-hidden group transition-all duration-300 cursor-pointer hover:-translate-y-1" class="site-card relative h-full flex flex-col overflow-hidden group transition-all duration-300"
:class="[ :class="[
cardProps.site?.is_active ? '' : 'opacity-70', cardProps.site?.is_active ? '' : 'opacity-70',
{ {
'border-error': statColor === 'error', 'border-error': statColor === 'error',
'border-warning': statColor === 'warning', 'border-warning': statColor === 'warning',
'border-success': statColor === 'success', 'border-success': statColor === 'success',
'cursor-pointer hover:-translate-y-1': !cardProps.sortable,
'cursor-move': cardProps.sortable,
'site-card--sortable': cardProps.sortable,
}, },
]" ]"
:ripple="false" :ripple="false"
variant="flat" variant="flat"
elevation="0" elevation="0"
rounded="lg" rounded="lg"
hover :hover="!cardProps.sortable"
@click="handleResourceBrowse" @click="handleCardClick"
> >
<!-- 装饰性状态指示器 --> <!-- 装饰性状态指示器 -->
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div> <div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
@@ -256,17 +275,37 @@ onMounted(() => {
<!-- 站点特性图标 --> <!-- 站点特性图标 -->
<div class="ml-auto flex shrink-0 items-center gap-2"> <div class="ml-auto flex shrink-0 items-center gap-2">
<div v-if="cardProps.site?.limit_interval" class="hover:bg-primary/8 transition-colors"> <div v-if="cardProps.site?.limit_interval" :class="cardProps.sortable ? '' : 'hover:bg-primary/8 transition-colors'">
<VIcon icon="mdi-speedometer" size="16" color="primary" class="opacity-85 hover:opacity-100" /> <VIcon
icon="mdi-speedometer"
size="16"
color="primary"
:class="cardProps.sortable ? 'opacity-85' : 'opacity-85 hover:opacity-100'"
/>
</div> </div>
<div v-if="cardProps.site?.proxy" class="hover:bg-primary/8 transition-colors"> <div v-if="cardProps.site?.proxy" :class="cardProps.sortable ? '' : 'hover:bg-primary/8 transition-colors'">
<VIcon icon="mdi-network-outline" size="16" color="primary" class="opacity-85 hover:opacity-100" /> <VIcon
icon="mdi-network-outline"
size="16"
color="primary"
:class="cardProps.sortable ? 'opacity-85' : 'opacity-85 hover:opacity-100'"
/>
</div> </div>
<div v-if="cardProps.site?.render" class="hover:bg-primary/8 transition-colors"> <div v-if="cardProps.site?.render" :class="cardProps.sortable ? '' : 'hover:bg-primary/8 transition-colors'">
<VIcon icon="mdi-apple-safari" size="16" color="primary" class="opacity-85 hover:opacity-100" /> <VIcon
icon="mdi-apple-safari"
size="16"
color="primary"
:class="cardProps.sortable ? 'opacity-85' : 'opacity-85 hover:opacity-100'"
/>
</div> </div>
<div v-if="cardProps.site?.filter" class="hover:bg-primary/8 transition-colors"> <div v-if="cardProps.site?.filter" :class="cardProps.sortable ? '' : 'hover:bg-primary/8 transition-colors'">
<VIcon icon="mdi-filter-cog-outline" size="16" color="primary" class="opacity-85 hover:opacity-100" /> <VIcon
icon="mdi-filter-cog-outline"
size="16"
color="primary"
:class="cardProps.sortable ? 'opacity-85' : 'opacity-85 hover:opacity-100'"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -274,10 +313,10 @@ onMounted(() => {
<!-- 中间部分网址 --> <!-- 中间部分网址 -->
<div class="my-3"> <div class="my-3">
<div class="min-w-0 truncate text-sm text-medium-emphasis" @click.stop="openSitePage"> <div class="min-w-0 truncate text-sm text-medium-emphasis" @click.stop="handleSiteUrlClick">
{{ cardProps.site?.url }} {{ cardProps.site?.url }}
</div>
</div> </div>
</div>
<!-- 底部数据统计 --> <!-- 底部数据统计 -->
<div class="flex-1 flex flex-col justify-end"> <div class="flex-1 flex flex-col justify-end">
@@ -309,7 +348,7 @@ onMounted(() => {
</div> </div>
<!-- 右侧操作按钮区 --> <!-- 右侧操作按钮区 -->
<VSheet class="site-card-actions absolute inset-y-0 right-0 z-20 flex flex-col py-2 px-1"> <VSheet v-if="!cardProps.sortable" class="site-card-actions absolute inset-y-0 right-0 z-20 flex flex-col py-2 px-1">
<!-- 测试按钮 --> <!-- 测试按钮 -->
<VBtn <VBtn
icon icon
@@ -432,7 +471,7 @@ onMounted(() => {
} }
/* 站点卡片悬停时状态指示器变化 */ /* 站点卡片悬停时状态指示器变化 */
.site-card:hover .site-status-indicator { .site-card:not(.site-card--sortable):hover .site-status-indicator {
block-size: 2px; block-size: 2px;
opacity: 0.8; opacity: 0.8;
} }
+18 -5
View File
@@ -313,6 +313,10 @@ function onSubscribeEditRemove() {
// 处理卡片点击事件 // 处理卡片点击事件
function handleCardClick() { function handleCardClick() {
if (props.sortable) {
return
}
if (props.batchMode) { if (props.batchMode) {
// 批量模式下触发选择事件 // 批量模式下触发选择事件
emit('select') emit('select')
@@ -330,7 +334,7 @@ function handleCardClick() {
<div <div
class="w-full h-full rounded-lg overflow-hidden" class="w-full h-full rounded-lg overflow-hidden"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
'outline-dashed outline-1': props.media?.best_version && imageLoaded, 'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected, 'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
}" }"
@@ -341,13 +345,14 @@ function handleCardClick() {
class="flex flex-col h-full" class="flex flex-col h-full"
:class="{ :class="{
'opacity-70': subscribeState === 'S', 'opacity-70': subscribeState === 'S',
'cursor-move': props.sortable,
}" }"
rounded="0" rounded="0"
min-height="150" min-height="150"
@click="handleCardClick" @click="handleCardClick"
:ripple="!props.batchMode" :ripple="!props.batchMode && !props.sortable"
> >
<div class="me-n3 absolute top-1 right-4"> <div v-if="!props.sortable" class="me-n3 absolute top-1 right-4">
<IconBtn> <IconBtn>
<VIcon icon="mdi-dots-vertical" color="white" /> <VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
@@ -405,8 +410,15 @@ function handleCardClick() {
</VCardText> </VCardText>
<VCardText class="flex justify-space-between align-center flex-wrap px-3"> <VCardText class="flex justify-space-between align-center flex-wrap px-3">
<div class="flex align-center"> <div class="flex align-center">
<VIcon
v-if="props.media?.total_episode && props.sortable"
icon="mdi-progress-download"
size="small"
color="white"
class="me-1"
/>
<IconBtn <IconBtn
v-if="props.media?.total_episode" v-else-if="props.media?.total_episode"
size="small" size="small"
v-bind="props" v-bind="props"
icon="mdi-progress-download" icon="mdi-progress-download"
@@ -416,7 +428,8 @@ function handleCardClick() {
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} / {{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }} {{ props.media?.total_episode }}
</div> </div>
<IconBtn v-if="props.media?.username" icon="mdi-account" size="small" color="white" /> <VIcon v-if="props.media?.username && props.sortable" icon="mdi-account" size="small" color="white" class="me-1" />
<IconBtn v-else-if="props.media?.username" icon="mdi-account" size="small" color="white" />
<span v-if="props.media?.username" class="text-subtitle-2 text-white"> <span v-if="props.media?.username" class="text-subtitle-2 text-white">
{{ props.media?.username }} {{ props.media?.username }}
</span> </span>
+1 -1
View File
@@ -75,7 +75,7 @@ export default {
versionMismatch: 'The browser cache version is inconsistent with the server version, please try to clear the cache', versionMismatch: 'The browser cache version is inconsistent with the server version, please try to clear the cache',
clearCache: 'Clear Cache', clearCache: 'Clear Cache',
sortMode: 'Sort Mode', sortMode: 'Sort Mode',
sortModeHint: 'Virtualization is disabled for large lists to allow drag sorting, which may reduce smoothness.', sortModeHint: 'Drag sorting mode is active',
}, },
mediaType: { mediaType: {
movie: 'Movie', movie: 'Movie',
+1 -1
View File
@@ -75,7 +75,7 @@ export default {
versionMismatch: '浏览器缓存版本与服务端版本不一致,请尝试清除缓存', versionMismatch: '浏览器缓存版本与服务端版本不一致,请尝试清除缓存',
clearCache: '清除缓存', clearCache: '清除缓存',
sortMode: '排序模式', sortMode: '排序模式',
sortModeHint: '已关闭大列表虚拟渲染,方便拖拽排序,但页面流畅度可能下降。', sortModeHint: '已进入拖拽排序模式',
}, },
mediaType: { mediaType: {
movie: '电影', movie: '电影',
+1 -1
View File
@@ -75,7 +75,7 @@ export default {
versionMismatch: '瀏覽器快取版本與服務端版本不一致,請嘗試清除快取', versionMismatch: '瀏覽器快取版本與服務端版本不一致,請嘗試清除快取',
clearCache: '清除快取', clearCache: '清除快取',
sortMode: '排序模式', sortMode: '排序模式',
sortModeHint: '已關閉大列表虛擬渲染,方便拖拽排序,但頁面流暢度可能下降。', sortModeHint: '已進入拖拽排序模式',
}, },
mediaType: { mediaType: {
movie: '電影', movie: '電影',
-2
View File
@@ -1536,7 +1536,6 @@ function onDragStartPlugin(evt: any) {
v-model="mixedSortList" v-model="mixedSortList"
@end="saveMixedSortOrder" @end="saveMixedSortOrder"
@start="onDragStartPlugin" @start="onDragStartPlugin"
handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
class="grid gap-4 grid-plugin-card" class="grid gap-4 grid-plugin-card"
@@ -1599,7 +1598,6 @@ function onDragStartPlugin(evt: any) {
v-model="draggableFolderPlugins" v-model="draggableFolderPlugins"
@end="saveFolderPluginOrder" @end="saveFolderPluginOrder"
@start="onDragStartPlugin" @start="onDragStartPlugin"
handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
class="grid gap-4 grid-plugin-card" class="grid gap-4 grid-plugin-card"
-1
View File
@@ -404,7 +404,6 @@ useDynamicButton({
v-if="draggableSiteList.length > 0 && canDragSort" v-if="draggableSiteList.length > 0 && canDragSort"
v-model="draggableSiteList" v-model="draggableSiteList"
@end="savaSitesPriority" @end="savaSitesPriority"
handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
:component-data="{ 'class': 'grid gap-4 grid-site-card px-2' }" :component-data="{ 'class': 'grid gap-4 grid-site-card px-2' }"
@@ -491,7 +491,6 @@ defineExpose({
v-if="displayList.length > 0 && canDragSort" v-if="displayList.length > 0 && canDragSort"
v-model="displayList" v-model="displayList"
@end="saveSubscribeOrder" @end="saveSubscribeOrder"
handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
:component-data="{ class: 'grid gap-4 grid-subscribe-card px-2' }" :component-data="{ class: 'grid gap-4 grid-subscribe-card px-2' }"