refactor: 统一组件样式,优化圆角和阴影效果,提升视觉一致性

This commit is contained in:
jxxghp
2026-06-07 19:49:15 +08:00
parent e239c0c5ea
commit c9ebf23977
17 changed files with 144 additions and 54 deletions
+1 -1
View File
@@ -346,7 +346,7 @@ function stopDrag() {
justify-content: center; justify-content: center;
background-color: transparent; background-color: transparent;
cursor: col-resize; cursor: col-resize;
inline-size: 4px; inline-size: 1px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
user-select: none; user-select: none;
} }
@@ -109,7 +109,14 @@ function submitSettings() {
</script> </script>
<template> <template>
<VDialog v-if="visible" v-model="visible" width="35rem" class="settings-dialog" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog
v-if="visible"
v-model="visible"
width="35rem"
class="settings-dialog"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard class="settings-card"> <VCard class="settings-card">
<VCardItem class="settings-card-header"> <VCardItem class="settings-card-header">
<VCardTitle> <VCardTitle>
@@ -195,6 +202,7 @@ function submitSettings() {
.setting-item { .setting-item {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-radius: var(--app-surface-radius);
background-color: rgba(var(--v-theme-surface-variant), 0.3); background-color: rgba(var(--v-theme-surface-variant), 0.3);
cursor: pointer; cursor: pointer;
padding-block: 10px; padding-block: 10px;
+6 -2
View File
@@ -90,7 +90,7 @@ function closeDialog() {
/> />
</VToolbar> </VToolbar>
<VDialogCloseBtn @click="closeDialog" /> <VDialogCloseBtn @click="closeDialog" />
<VList v-if="plugins.length > 0" lines="two"> <VList v-if="plugins.length > 0" class="plugin-search-list" lines="two">
<VVirtualScroll :items="plugins"> <VVirtualScroll :items="plugins">
<template #default="{ item }"> <template #default="{ item }">
<VListItem @click="emit('open-plugin', item)"> <VListItem @click="emit('open-plugin', item)">
@@ -130,4 +130,8 @@ function closeDialog() {
</VCard> </VCard>
</VDialog> </VDialog>
</template> </template>
<style lang="scss" scoped>
.plugin-search-list {
border-radius: 0 !important;
}
</style>
+5 -2
View File
@@ -129,7 +129,7 @@ const filteredSites = computed(() => {
<div <div
v-bind="props" v-bind="props"
:class="[ :class="[
'site-checkbox-wrapper pa-2 pa-sm-3 rounded-lg d-flex align-center', 'site-checkbox-wrapper pa-2 pa-sm-3 d-flex align-center',
{ {
'site-selected': selectedSites.includes(site.id), 'site-selected': selectedSites.includes(site.id),
'site-hover': isHovering && !selectedSites.includes(site.id), 'site-hover': isHovering && !selectedSites.includes(site.id),
@@ -193,8 +193,11 @@ const filteredSites = computed(() => {
<style scoped> <style scoped>
.site-checkbox-wrapper { .site-checkbox-wrapper {
border: 1px solid rgba(var(--v-theme-on-surface), 0.08); border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
border-radius: var(--app-surface-radius);
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease; transition:
transform 0.2s ease,
background-color 0.2s ease;
} }
.site-checkbox-wrapper:hover { .site-checkbox-wrapper:hover {
@@ -216,11 +216,50 @@ function getMediaTypeText(type: string | undefined) {
</VVirtualScroll> </VVirtualScroll>
</VInfiniteScroll> </VInfiniteScroll>
</VList> </VList>
<VCardText v-if="historyList.length === 0 && isRefreshed" class="text-center">{{ <VCardText v-if="historyList.length === 0 && isRefreshed" class="subscribe-history-empty">
t('dialog.subscribeHistory.noData') <VIcon class="subscribe-history-empty__icon" icon="mdi-sync" size="30" />
}}</VCardText>
<div class="subscribe-history-empty__headline">
{{ t('dialog.subscribeHistory.noData') }}
</div>
<div class="subscribe-history-empty__description">
{{ t('dialog.subscribeHistory.noDataHint') }}
</div>
</VCardText>
</VCard> </VCard>
<!-- 进度框 --> <!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" /> <ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</VDialog> </VDialog>
</template> </template>
<style scoped>
.subscribe-history-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
min-block-size: 13rem;
padding-block: 2.5rem !important;
padding-inline: 1.5rem !important;
text-align: center;
}
.subscribe-history-empty__icon {
color: rgba(var(--v-theme-on-surface), 0.32);
}
.subscribe-history-empty__headline {
color: rgba(var(--v-theme-on-surface), 0.9);
font-size: 1.15rem;
font-weight: 600;
line-height: 1.4;
}
.subscribe-history-empty__description {
color: rgba(var(--v-theme-on-surface), 0.6);
font-size: 0.92rem;
line-height: 1.65;
max-inline-size: 25rem;
}
</style>
+1 -2
View File
@@ -29,8 +29,7 @@ const display = useDisplay()
const { appMode } = usePWA() const { appMode } = usePWA()
// 计算列表可用高度 // 计算列表可用高度
// componentOffset = FileToolbar(48) + FileList操作栏(40) + VCard边距(4) = 92 const { availableHeight: listAvailableHeight } = useAvailableHeight(100, 300)
const { availableHeight: listAvailableHeight } = useAvailableHeight(89, 300)
// 输入参数 // 输入参数
const inProps = defineProps({ const inProps = defineProps({
+1 -6
View File
@@ -4,7 +4,6 @@ import type { FileItem } from '@/api/types'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import type { AxiosRequestConfig, AxiosInstance } from 'axios' import type { AxiosRequestConfig, AxiosInstance } from 'axios'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { usePWA } from '@/composables/usePWA'
import { useAvailableHeight } from '@/composables/useAvailableHeight' import { useAvailableHeight } from '@/composables/useAvailableHeight'
// 国际化 // 国际化
@@ -12,16 +11,13 @@ const { t } = useI18n()
const display = useDisplay() const display = useDisplay()
const { appMode } = usePWA()
type TreeRow = type TreeRow =
| { type: 'root'; key: string; level: number } | { type: 'root'; key: string; level: number }
| { type: 'loading'; key: string; path: string; level: number } | { type: 'loading'; key: string; path: string; level: number }
| { type: 'directory'; key: string; dir: FileItem; level: number } | { type: 'directory'; key: string; dir: FileItem; level: number }
// 计算列表可用高度 // 计算列表可用高度
// componentOffset = FileToolbar(48) = 48 const { availableHeight } = useAvailableHeight(58, 300)
const { availableHeight } = useAvailableHeight(48, 300)
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -326,7 +322,6 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 0;
border-radius: 0 !important; border-radius: 0 !important;
background: rgb(var(--v-table-header-background));
block-size: 100%; block-size: 100%;
box-shadow: none !important; box-shadow: none !important;
inline-size: 240px; inline-size: 240px;
+1
View File
@@ -2790,6 +2790,7 @@ export default {
resubscribeTv: 'Resubscribing {name} Season {season}...', resubscribeTv: 'Resubscribing {name} Season {season}...',
season: 'Season {season}', season: 'Season {season}',
noData: 'No completed subscriptions', noData: 'No completed subscriptions',
noDataHint: 'Completed subscription history will be displayed here',
}, },
siteUserData: { siteUserData: {
title: 'Site User Data', title: 'Site User Data',
+1
View File
@@ -2740,6 +2740,7 @@ export default {
resubscribeTv: '正在重新订阅 {name} 第 {season} 季...', resubscribeTv: '正在重新订阅 {name} 第 {season} 季...',
season: '第 {season} 季', season: '第 {season} 季',
noData: '没有已完成的订阅', noData: '没有已完成的订阅',
noDataHint: '完成的订阅会显示在这里',
}, },
siteUserData: { siteUserData: {
title: '站点用户数据', title: '站点用户数据',
+1
View File
@@ -2741,6 +2741,7 @@ export default {
resubscribeTv: '正在重新訂閱 {name} 第 {season} 季...', resubscribeTv: '正在重新訂閱 {name} 第 {season} 季...',
season: '第 {season} 季', season: '第 {season} 季',
noData: '沒有已完成的訂閱', noData: '沒有已完成的訂閱',
noDataHint: '完成的訂閱會顯示在這裡',
}, },
siteUserData: { siteUserData: {
title: '站點用戶數據', title: '站點用戶數據',
+50 -15
View File
@@ -148,6 +148,15 @@ html[data-theme-shadow='high'] {
transition: border-color 0.2s ease, border-radius 0.2s ease, box-shadow 0.2s ease; transition: border-color 0.2s ease, border-radius 0.2s ease, box-shadow 0.2s ease;
} }
.app-surface-static {
border-radius: var(--app-surface-radius) !important;
box-shadow: var(--app-surface-shadow) !important;
}
.app-surface-static:hover {
box-shadow: var(--app-surface-shadow) !important;
}
// 底部导航卡片原本就是胶囊形态不参与主题圆角切换 // 底部导航卡片原本就是胶囊形态不参与主题圆角切换
.footer-nav-card.v-card { .footer-nav-card.v-card {
--app-surface-radius: 9999px; --app-surface-radius: 9999px;
@@ -183,21 +192,52 @@ html[data-theme-shadow='high'] {
} }
// 统一文本输入下拉框文本域等有框表单控件的圆角显式 rounded 控件保留特殊形态 // 统一文本输入下拉框文本域等有框表单控件的圆角显式 rounded 控件保留特殊形态
.v-field:not(.v-field--variant-plain, .v-field--variant-underlined, .v-field--rounded, [class^='rounded-'], [class*=' rounded-']) { .v-field:not(
.v-field--variant-plain,
.v-field--variant-underlined,
.v-field--rounded,
[class^='rounded-'],
[class*=' rounded-']
) {
border-radius: var(--app-field-radius); border-radius: var(--app-field-radius);
transition: border-radius 0.2s ease, box-shadow 0.2s ease; transition: border-radius 0.2s ease, box-shadow 0.2s ease;
} }
.v-btn:not(.v-btn--variant-plain, .v-btn--variant-text) { // 自定义字段圆角时outlined 字段带左侧 inner icon 需要同步放大左侧描边弧度
.v-field:not(
.v-field--variant-plain,
.v-field--variant-underlined,
.v-field--rounded,
[class^='rounded-'],
[class*=' rounded-']
).v-field--variant-outlined.v-field--prepended {
--app-field-outline-start-size: max(12px, calc(var(--app-field-radius) + 8px));
.v-field__outline__start {
flex-basis: var(--app-field-outline-start-size);
}
.v-field__outline__notch {
max-inline-size: calc(100% - var(--app-field-outline-start-size));
}
}
.v-btn:not(.v-btn--variant-plain, .v-btn--variant-text, .v-btn--flat) {
box-shadow: var(--app-surface-shadow) !important; box-shadow: var(--app-surface-shadow) !important;
transition: box-shadow 0.2s ease; transition: box-shadow 0.2s ease;
} }
.v-btn:not(.v-btn--rounded, [class^='rounded-'], [class*=' rounded-']) { .v-btn:not(.v-btn--rounded, .v-btn--flat, .v-btn--icon, [class^='rounded-'], [class*=' rounded-']) {
border-radius: var(--app-surface-radius); border-radius: var(--app-surface-radius);
transition: border-radius 0.2s ease; transition: border-radius 0.2s ease;
} }
.v-btn-group:not(.v-btn-group--variant-plain, .v-btn-group--variant-text) {
border-radius: var(--app-surface-radius);
box-shadow: var(--app-surface-shadow) !important;
transition: box-shadow 0.2s ease, border-radius 0.2s ease;
}
// 只给外层 surface 加边框和阴影卡片内部的列表表格等子组件保持平面避免层级噪声 // 只给外层 surface 加边框和阴影卡片内部的列表表格等子组件保持平面避免层级噪声
.v-card .v-list:not(.app-surface), .v-card .v-list:not(.app-surface),
.v-card .v-sheet:not(.app-surface), .v-card .v-sheet:not(.app-surface),
@@ -246,6 +286,7 @@ html[data-theme-skin='bordered'] {
// 设置项强调卡片复用通知模板入口的弱强调条轻渐变与悬浮反馈 // 设置项强调卡片复用通知模板入口的弱强调条轻渐变与悬浮反馈
.app-card-colorful { .app-card-colorful {
overflow: hidden; overflow: hidden;
border-radius: var(--app-surface-radius);
background: background:
linear-gradient( linear-gradient(
135deg, 135deg,
@@ -254,8 +295,9 @@ html[data-theme-skin='bordered'] {
rgba(var(--v-theme-surface), 0) 76% rgba(var(--v-theme-surface), 0) 76%
), ),
rgba(var(--v-theme-surface), var(--app-card-surface-opacity)); rgba(var(--v-theme-surface), var(--app-card-surface-opacity));
box-shadow: var(--app-card-rest-shadow); box-shadow: var(--app-surface-shadow);
color: rgb(var(--v-theme-on-surface)); color: rgb(var(--v-theme-on-surface));
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
--app-card-accent-rgb: var(--v-theme-primary); --app-card-accent-rgb: var(--v-theme-primary);
--app-card-accent-end-rgb: var(--app-card-accent-rgb); --app-card-accent-end-rgb: var(--app-card-accent-rgb);
@@ -268,8 +310,6 @@ html[data-theme-skin='bordered'] {
--app-surface-border: 1px solid rgba(var(--app-card-accent-rgb), var(--app-card-border-opacity)); --app-surface-border: 1px solid rgba(var(--app-card-accent-rgb), var(--app-card-border-opacity));
--app-surface-hover-shadow: var(--app-card-hover-shadow); --app-surface-hover-shadow: var(--app-card-hover-shadow);
--app-surface-shadow: var(--app-card-rest-shadow); --app-surface-shadow: var(--app-card-rest-shadow);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
} }
.app-card-colorful::before { .app-card-colorful::before {
@@ -459,10 +499,11 @@ html[data-theme="transparent"] .app-card-colorful,
padding-block: 4.5rem; padding-block: 4.5rem;
padding-inline: 1rem; padding-inline: 1rem;
} }
}
.Vue-Toastification__toast { .Vue-Toastification__toast {
border-radius: 8px; border-radius: var(--app-surface-radius);
} box-shadow: var(--app-overlay-shadow);
} }
// 对话框样式 // 对话框样式
@@ -623,11 +664,6 @@ html[data-theme="transparent"] .app-card-colorful,
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
} }
.Vue-Toastification__toast {
border-radius: var(--app-surface-radius);
box-shadow: var(--app-overlay-shadow);
}
.backdrop-blur { .backdrop-blur {
--tw-backdrop-blur: blur(8px)!important; --tw-backdrop-blur: blur(8px)!important;
@@ -794,7 +830,6 @@ html[data-theme="transparent"] .app-card-colorful,
// 弹出层样式 // 弹出层样式
.v-overlay__content .v-list{ .v-overlay__content .v-list{
overflow: hidden;
border-radius: var(--app-surface-radius); border-radius: var(--app-surface-radius);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.9); background-color: rgb(var(--v-theme-surface), 0.9);
+5 -3
View File
@@ -24,7 +24,9 @@ const route = useRoute()
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default)) const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const PluginAppCard = defineAsyncComponent(() => import('@/components/cards/PluginAppCard.vue')) const PluginAppCard = defineAsyncComponent(() => import('@/components/cards/PluginAppCard.vue'))
const PluginFolderCreateDialog = defineAsyncComponent(() => import('@/components/dialog/PluginFolderCreateDialog.vue')) const PluginFolderCreateDialog = defineAsyncComponent(() => import('@/components/dialog/PluginFolderCreateDialog.vue'))
const PluginMarketSettingDialog = defineAsyncComponent(() => import('@/components/dialog/PluginMarketSettingDialog.vue')) const PluginMarketSettingDialog = defineAsyncComponent(
() => import('@/components/dialog/PluginMarketSettingDialog.vue'),
)
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue')) const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
const PluginSearchDialog = defineAsyncComponent(() => import('@/components/dialog/PluginSearchDialog.vue')) const PluginSearchDialog = defineAsyncComponent(() => import('@/components/dialog/PluginSearchDialog.vue'))
@@ -1643,8 +1645,8 @@ function onDragStartPlugin(evt: any) {
<div> <div>
<VPageContentTitle v-if="installedFilter" :title="t('plugin.filter', { name: installedFilter })" /> <VPageContentTitle v-if="installedFilter" :title="t('plugin.filter', { name: installedFilter })" />
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4"> <VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 py-0 app-surface-static">
<div class="d-flex flex-wrap align-center justify-space-between gap-2"> <div class="d-flex flex-wrap align-center justify-space-between gap-2 py-5">
<span>{{ t('common.sortModeHint') }}</span> <span>{{ t('common.sortModeHint') }}</span>
<VBtn variant="tonal" color="error" @click="sortMode = false"> <VBtn variant="tonal" color="error" @click="sortMode = false">
{{ t('common.exit') }} {{ t('common.exit') }}
+1 -1
View File
@@ -204,7 +204,7 @@ onMounted(loadDownloadDirectories)
</script> </script>
<template> <template>
<div class="file-browser-view app-surface"> <div class="file-browser-view app-surface-static">
<FileBrowser <FileBrowser
v-if="operItem" v-if="operItem"
:storages="storages" :storages="storages"
+4 -4
View File
@@ -18,7 +18,9 @@ import { useBackground } from '@/composables/useBackground'
import { useGlobalSettingsStore } from '@/stores' import { useGlobalSettingsStore } from '@/stores'
import { openSharedDialog } from '@/composables/useSharedDialog' import { openSharedDialog } from '@/composables/useSharedDialog'
const TransferHistoryDeleteDialog = defineAsyncComponent(() => import('@/components/dialog/TransferHistoryDeleteDialog.vue')) const TransferHistoryDeleteDialog = defineAsyncComponent(
() => import('@/components/dialog/TransferHistoryDeleteDialog.vue'),
)
// i18n // i18n
const { t } = useI18n() const { t } = useI18n()
@@ -33,8 +35,7 @@ const { appMode } = usePWA()
const { useProgressSSE } = useBackground() const { useProgressSSE } = useBackground()
// //
// componentOffset = VCardItem(68) + VDivider(1) + (40) + VCard(2) = 111 const { availableHeight } = useAvailableHeight(135, 300)
const { availableHeight } = useAvailableHeight(125, 300)
// //
const $toast = useToast() const $toast = useToast()
@@ -1209,7 +1210,6 @@ onUnmounted(() => {
/> />
</div> </div>
</Teleport> </Teleport>
</template> </template>
<style lang="scss"> <style lang="scss">
+2 -2
View File
@@ -410,8 +410,8 @@ useDynamicButton({
</div> </div>
</div> </div>
<VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4"> <VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 py-0 app-surface-static">
<div class="d-flex flex-wrap align-center justify-space-between gap-2"> <div class="d-flex flex-wrap align-center justify-space-between gap-2 py-5">
<span>{{ t('common.sortModeHint') }}</span> <span>{{ t('common.sortModeHint') }}</span>
<VBtn variant="tonal" color="error" @click="sortMode = false"> <VBtn variant="tonal" color="error" @click="sortMode = false">
{{ t('common.exit') }} {{ t('common.exit') }}
+12 -10
View File
@@ -31,7 +31,12 @@ let progressDialogController: ReturnType<typeof openSharedDialog> | null = null
// //
function openProgressDialog() { function openProgressDialog() {
progressDialogController?.close() progressDialogController?.close()
progressDialogController = openSharedDialog(ProgressDialog, { text: `${t('common.loading')} ...` }, {}, { closeOn: false }) progressDialogController = openSharedDialog(
ProgressDialog,
{ text: `${t('common.loading')} ...` },
{},
{ closeOn: false },
)
} }
// //
@@ -85,7 +90,10 @@ async function eventsHander(subscribe: Subscribe) {
} else { } else {
// API // API
const params = subscribe.episode_group ? { episode_group: subscribe.episode_group } : undefined const params = subscribe.episode_group ? { episode_group: subscribe.episode_group } : undefined
const episodes: TmdbEpisode[] = await api.get(`tmdb/${subscribe.tmdbid}/${subscribe.season}`, params ? { params } : undefined) const episodes: TmdbEpisode[] = await api.get(
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
params ? { params } : undefined,
)
interface EpisodeInfo { interface EpisodeInfo {
title: string title: string
@@ -160,8 +168,8 @@ onActivated(() => {
<template> <template>
<FullCalendar :options="calendarOptions"> <FullCalendar :options="calendarOptions">
<template #eventContent="arg"> <template #eventContent="arg">
<div class="hidden md:block overflow-hidden"> <div class="hidden md:block">
<VCard> <VCard class="app-surface">
<div class="d-flex justify-space-between flex-nowrap flex-row"> <div class="d-flex justify-space-between flex-nowrap flex-row">
<div class="ma-auto"> <div class="ma-auto">
<VImg <VImg
@@ -434,12 +442,6 @@ onActivated(() => {
background-color: transparent !important; background-color: transparent !important;
} }
.v-application .fc .fc-popover {
border-radius: 6px;
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity), 0 4px 8px -4px var(--v-shadow-key-penumbra-opacity),
0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
}
.v-application .fc .fc-popover .fc-popover-header, .v-application .fc .fc-popover .fc-popover-header,
.v-application .fc .fc-popover .fc-popover-body { .v-application .fc .fc-popover .fc-popover-body {
padding: 0.5rem; padding: 0.5rem;
+2 -2
View File
@@ -572,8 +572,8 @@ defineExpose({
</VCard> </VCard>
</div> </div>
<VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 mx-2"> <VAlert v-if="sortMode" color="warning" variant="tonal" class="mb-4 mx-2 py-0 app-surface-static">
<div class="d-flex flex-wrap align-center justify-space-between gap-2"> <div class="d-flex flex-wrap align-center justify-space-between gap-2 py-5">
<span>{{ t('common.sortModeHint') }}</span> <span>{{ t('common.sortModeHint') }}</span>
<VBtn variant="tonal" color="error" @click="sortMode = false"> <VBtn variant="tonal" color="error" @click="sortMode = false">
{{ t('common.exit') }} {{ t('common.exit') }}