refine settings card layouts

This commit is contained in:
jxxghp
2026-04-21 08:50:14 +08:00
parent 7532d39978
commit a2bf0d2b16
9 changed files with 199 additions and 50 deletions
+8 -6
View File
@@ -101,19 +101,21 @@ function onClose() {
<template> <template>
<div> <div>
<VCard variant="tonal" @click="openRuleInfoDialog"> <VCard variant="tonal" class="app-card-shell" @click="openRuleInfoDialog">
<span class="absolute top-3 right-12"> <span class="absolute top-3 right-12">
<IconBtn> <IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" /> <VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn> </IconBtn>
</span> </span>
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="app-card-summary app-card-summary--double-action app-card-summary--title-subtitle">
<div class="align-self-start"> <div class="app-card-summary__content">
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5> <h5 class="app-card-summary__title text-h6">{{ props.rule.name }}</h5>
<div class="text-body-1 mb-3">{{ props.rule.id }}</div> <div class="app-card-summary__subtitle text-body-1">{{ props.rule.id }}</div>
</div>
<div class="app-card-summary__media" aria-hidden="true">
<VImg :src="filter_svg" contain class="app-card-summary__image" />
</div> </div>
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText> </VCardText>
</VCard> </VCard>
<VDialog <VDialog
+1 -1
View File
@@ -195,7 +195,7 @@ watch(
</script> </script>
<template> <template>
<VCard variant="tonal" :width="props.width" :height="props.height"> <VCard variant="tonal" class="app-card-shell" :width="props.width" :height="props.height">
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardItem> <VCardItem>
<VTextField <VTextField
+15 -11
View File
@@ -252,6 +252,7 @@ onUnmounted(() => {
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"
variant="tonal" variant="tonal"
class="app-card-shell"
@click="openDownloaderInfoDialog" @click="openDownloaderInfoDialog"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }" :class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
> >
@@ -261,9 +262,9 @@ onUnmounted(() => {
<VIcon class="cursor-move" icon="mdi-drag" /> <VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn> </IconBtn>
</span> </span>
<VCardText class="flex justify-space-between align-center gap-4"> <VCardText class="app-card-summary app-card-summary--double-action">
<div class="align-self-start flex-1"> <div class="app-card-summary__content">
<div class="flex items-center"> <div class="app-card-summary__title-row">
<VBadge <VBadge
v-if="props.downloader.default && props.downloader.enabled" v-if="props.downloader.default && props.downloader.enabled"
dot dot
@@ -271,18 +272,21 @@ onUnmounted(() => {
color="success" color="success"
class="me-1" class="me-1"
/> />
<span class="text-h6">{{ downloader.name }}</span> <span class="app-card-summary__title text-h6">{{ downloader.name }}</span>
</div> </div>
<div v-if="downloaderDict[downloader.type] && props.downloader.enabled" class="mt-1 flex flex-wrap text-sm"> <div
<span class="me-2">{{ `${formatFileSize(upload_rate, 1)}/s ` }}</span> v-if="downloaderDict[downloader.type] && props.downloader.enabled"
<span>{{ `${formatFileSize(download_rate, 1)}/s` }}</span> class="app-card-summary__meta text-sm"
>
<span class="app-card-summary__meta-item">{{ `${formatFileSize(upload_rate, 1)}/s` }}</span>
<span class="app-card-summary__meta-item">{{ `${formatFileSize(download_rate, 1)}/s` }}</span>
</div> </div>
<div v-else-if="!downloaderDict[downloader.type]" class="mt-1 flex flex-wrap text-sm"> <div v-else-if="!downloaderDict[downloader.type]" class="app-card-summary__subtitle text-sm">
<span class="me-2">自定义下载器</span> 自定义下载器
</div> </div>
</div> </div>
<div class="h-20"> <div class="app-card-summary__media" aria-hidden="true">
<VImg :src="getIcon" cover class="mt-8 me-3" max-width="3rem" min-width="3rem" /> <VImg :src="getIcon" contain class="app-card-summary__image" />
</div> </div>
</VCardText> </VCardText>
</VCard> </VCard>
+2 -2
View File
@@ -45,7 +45,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<VCard variant="tonal"> <VCard variant="tonal" class="app-card-shell">
<span class="absolute top-3 right-12"> <span class="absolute top-3 right-12">
<IconBtn> <IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" /> <VIcon class="cursor-move" icon="mdi-drag" />
@@ -53,7 +53,7 @@ onMounted(() => {
</span> </span>
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardItem> <VCardItem>
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle> <VCardTitle class="pr-8">{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
<VRow> <VRow>
<VCol> <VCol>
<VAutocomplete <VAutocomplete
+8 -6
View File
@@ -205,22 +205,24 @@ function onClose() {
<template> <template>
<div> <div>
<VCard variant="tonal" @click="opengroupInfoDialog"> <VCard variant="tonal" class="app-card-shell" @click="opengroupInfoDialog">
<span class="absolute top-3 right-12"> <span class="absolute top-3 right-12">
<IconBtn> <IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" /> <VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn> </IconBtn>
</span> </span>
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="app-card-summary app-card-summary--double-action app-card-summary--title-subtitle">
<div class="align-self-start"> <div class="app-card-summary__content">
<h5 class="text-h6 mb-1">{{ props.group.name }}</h5> <h5 class="app-card-summary__title text-h6">{{ props.group.name }}</h5>
<div class="text-body-1 mb-3"> <div class="app-card-summary__subtitle text-body-1">
<span v-if="!props.group.category">{{ props.group.media_type || t('common.all') }}</span> <span v-if="!props.group.category">{{ props.group.media_type || t('common.all') }}</span>
<span v-else>{{ props.group.category }}</span> <span v-else>{{ props.group.category }}</span>
</div> </div>
</div> </div>
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" /> <div class="app-card-summary__media" aria-hidden="true">
<VImg :src="filter_group_svg" contain class="app-card-summary__image" />
</div>
</VCardText> </VCardText>
</VCard> </VCard>
<VDialog <VDialog
+16 -10
View File
@@ -199,21 +199,27 @@ onMounted(() => {
</script> </script>
<template> <template>
<div> <div>
<VCard variant="tonal" @click="openMediaServerInfoDialog"> <VCard variant="tonal" class="app-card-shell" @click="openMediaServerInfoDialog">
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="app-card-summary app-card-summary--single-action">
<div class="align-self-start flex-1"> <div class="app-card-summary__content">
<div class="text-h6 mb-1">{{ mediaserver.name }}</div> <div class="app-card-summary__title text-h6">{{ mediaserver.name }}</div>
<div v-if="mediaServerDict[mediaserver.type] && mediaserver.enabled" class="text-sm mt-5 flex flex-wrap"> <div
<span v-for="item in infoItems" :key="item.title" class="me-2 mb-1"> v-if="mediaServerDict[mediaserver.type] && mediaserver.enabled"
<VIcon rounded :icon="item.avatar" class="me-1" />{{ item.amount }} class="grid min-h-6 grid-cols-3 gap-2 text-sm text-medium-emphasis"
>
<span v-for="item in infoItems" :key="item.title" class="flex min-w-0 items-center">
<VIcon rounded :icon="item.avatar" class="me-1 shrink-0" />
<span class="truncate">{{ item.amount }}</span>
</span> </span>
</div> </div>
<div v-else-if="!mediaServerDict[mediaserver.type]" class="text-sm mt-5 flex flex-wrap"> <div v-else-if="!mediaServerDict[mediaserver.type]" class="app-card-summary__subtitle text-sm">
<span class="me-2 mb-1">自定义媒体服务器</span> 自定义媒体服务器
</div> </div>
</div> </div>
<VImg :src="getIcon" class="mt-8 me-3 max-h-12" max-width="3rem" min-width="3rem" /> <div class="app-card-summary__media" aria-hidden="true">
<VImg :src="getIcon" contain class="app-card-summary__image" />
</div>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -153,22 +153,24 @@ function onClose() {
</script> </script>
<template> <template>
<div> <div>
<VCard variant="tonal" @click="openNotificationInfoDialog"> <VCard variant="tonal" class="app-card-shell" @click="openNotificationInfoDialog">
<span class="absolute top-3 right-12"> <span class="absolute top-3 right-12">
<IconBtn> <IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" /> <VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn> </IconBtn>
</span> </span>
<VDialogCloseBtn @click="onClose" /> <VDialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="app-card-summary app-card-summary--double-action">
<div class="align-self-start"> <div class="app-card-summary__content">
<div class="flex items-center"> <div class="app-card-summary__title-row">
<VBadge v-if="props.notification.enabled" dot inline color="success" class="me-1" /> <VBadge v-if="props.notification.enabled" dot inline color="success" class="me-1" />
<span class="text-h6">{{ props.notification.name }}</span> <span class="app-card-summary__title text-h6">{{ props.notification.name }}</span>
</div> </div>
<div class="text-body-1 mb-3">{{ notificationTypeNames[notification.type] }}</div> <div class="app-card-summary__subtitle text-body-1">{{ notificationTypeNames[notification.type] }}</div>
</div>
<div class="app-card-summary__media" aria-hidden="true">
<VImg :src="getIcon" contain class="app-card-summary__image" />
</div> </div>
<VImg :src="getIcon" cover class="mt-7 me-1" max-width="3rem" />
</VCardText> </VCardText>
</VCard> </VCard>
+13 -7
View File
@@ -216,11 +216,17 @@ onMounted(() => {
<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>
<!-- 主体部分 --> <!-- 主体部分 -->
<div class="relative flex-1 flex flex-col p-3 z-1"> <div class="relative z-1 flex flex-1 flex-col p-3 pr-12">
<!-- 顶部图标和站点名称 --> <!-- 顶部图标和站点名称 -->
<div class="flex items-center mb-1"> <div class="mb-1 flex min-w-0 items-center gap-2">
<!-- 站点图标 --> <!-- 站点图标 -->
<VAvatar tile rounded="lg" size="32" class="me-2" :class="{ 'cursor-move': display.mdAndUp.value }"> <VAvatar
tile
rounded="lg"
size="32"
class="shrink-0"
:class="{ 'cursor-move': display.mdAndUp.value }"
>
<VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover> <VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover>
<template #placeholder> <template #placeholder>
<div class="w-full h-full"> <div class="w-full h-full">
@@ -231,11 +237,11 @@ onMounted(() => {
</VAvatar> </VAvatar>
<!-- 站点名称和特性图标 --> <!-- 站点名称和特性图标 -->
<div class="flex-1 min-w-0 flex items-center"> <div class="flex min-w-0 flex-1 items-center gap-2">
<h3 class="text-lg font-semibold leading-tight truncate">{{ cardProps.site?.name }}</h3> <h3 class="min-w-0 flex-1 truncate text-lg font-semibold leading-tight">{{ cardProps.site?.name }}</h3>
<!-- 站点特性图标 --> <!-- 站点特性图标 -->
<div class="flex items-center gap-2 ml-auto mr-10"> <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="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="opacity-85 hover:opacity-100" />
</div> </div>
@@ -254,7 +260,7 @@ onMounted(() => {
<!-- 中间部分网址 --> <!-- 中间部分网址 -->
<div class="my-3"> <div class="my-3">
<div class="text-sm text-medium-emphasis truncate" @click.stop="openSitePage"> <div class="min-w-0 truncate text-sm text-medium-emphasis" @click.stop="openSitePage">
{{ cardProps.site?.url }} {{ cardProps.site?.url }}
</div> </div>
</div> </div>
+127
View File
@@ -48,6 +48,133 @@ html.v-overlay-scroll-blocked body {
} }
} }
// 应用类信息卡片:固定右侧媒体槽位,避免图片被左侧文字挤压变形
.app-card-shell {
block-size: 100%;
}
.app-card-summary {
position: relative;
display: flex;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
min-block-size: 7.5rem;
block-size: 7.5rem;
}
.app-card-summary__content {
position: relative;
z-index: 1;
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
min-inline-size: 0;
row-gap: 0.25rem;
padding-block: 0.25rem 0.5rem;
}
.app-card-summary__title-row {
display: flex;
align-items: flex-start;
min-inline-size: 0;
column-gap: 0.25rem;
}
.app-card-summary__subtitle,
.app-card-summary__meta-item {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-card-summary__title {
display: -webkit-box;
flex: 0 0 auto;
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-break: break-word;
line-height: 1.35;
max-block-size: calc(1.35em * 2);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.app-card-summary__title-row .app-card-summary__title {
flex: 1 1 auto;
}
.app-card-summary__meta {
display: flex;
align-items: center;
min-inline-size: 0;
min-block-size: 1.5rem;
column-gap: 0.5rem;
overflow: hidden;
}
.app-card-summary--single-action .app-card-summary__content {
padding-inline-end: 3.75rem;
}
.app-card-summary--double-action .app-card-summary__content {
padding-inline-end: 5rem;
}
.app-card-summary--title-subtitle {
padding-block: 0.75rem !important;
}
.app-card-summary--title-subtitle .app-card-summary__content {
justify-content: space-between;
block-size: 100%;
padding-block: 0;
}
.app-card-summary--title-subtitle .app-card-summary__title {
flex: 0 1 auto;
}
.app-card-summary--title-subtitle .app-card-summary__subtitle {
flex-shrink: 0;
}
.app-card-summary__media {
position: absolute;
z-index: 0;
display: flex;
align-items: flex-end;
justify-content: flex-end;
pointer-events: none;
filter: brightness(1.35) saturate(1.05);
inset-block-end: 0.75rem;
inset-inline-end: 1rem;
}
.app-card-summary--single-action .app-card-summary__media,
.app-card-summary--double-action .app-card-summary__media {
inset-inline-end: 1rem;
}
.app-card-summary__image {
flex-shrink: 0;
inline-size: 3.5rem;
min-inline-size: 3.5rem;
max-inline-size: 3.5rem;
block-size: 3.5rem;
min-block-size: 3.5rem;
max-block-size: 3.5rem;
}
.app-card-summary__image .v-img__img {
object-fit: contain;
}
// Toast通知样式 // Toast通知样式
.Vue-Toastification__container { .Vue-Toastification__container {
z-index: 2500; z-index: 2500;