Refine dashboard layout drafts and episode status badges

This commit is contained in:
jxxghp
2026-07-09 19:33:27 +08:00
parent 728ced475b
commit f863a09212
2 changed files with 92 additions and 40 deletions

View File

@@ -121,12 +121,15 @@ const isPersistingDashboardGridLayoutFromGrid = ref(false)
// 仪表板本地布局覆盖配置
const dashboardGridLayout = ref<DashboardGridLayoutConfig>({})
// 最近一次已确认持久化的仪表板布局,用于编辑模式下避开临时布局草稿。
let persistedDashboardGridLayout: DashboardGridLayoutConfig = {}
// 是否处于“恢复默认布局”的临时草稿,确认前保持清空布局覆盖的语义。
const isDashboardGridLayoutResetDraft = ref(false)
// 当前仪表板布局档位,按 GridStack 响应式列数拆分跨端配置。
const dashboardLayoutProfile = ref<DashboardLayoutProfile>('desktop')
// 是否刚恢复过默认布局,用于避免退出编辑时立即把默认布局写回本地覆盖。
const isDashboardGridLayoutResetPending = ref(false)
// 旧版跨设备显示项配置,仅用于首次迁移到按设备拆分的仪表盘配置。
let legacyDashboardEnableConfig: DashboardEnableConfig | undefined
let isLegacyDashboardEnableConfigLoaded = false
@@ -720,23 +723,34 @@ async function loadSharedDashboardConfig<T>(
return localConfig
}
// 将当前仪表板布局覆盖配置保存到本地和用户配置。
function saveDashboardProfileConfig(layout = dashboardGridLayout.value, enabled = enableConfig.value) {
// 保存指定布局或当前已确认布局到本地和用户配置。
function saveDashboardProfileConfig(layout?: DashboardGridLayoutConfig, enabled = enableConfig.value) {
const profile = dashboardLayoutProfile.value
const profileConfig = buildDashboardProfileConfig(layout, enabled)
const layoutToPersist = layout ?? (isLayoutEditing.value ? persistedDashboardGridLayout : dashboardGridLayout.value)
const profileConfig = buildDashboardProfileConfig(layoutToPersist, enabled)
saveLocalDashboardConfig(getDashboardGridLayoutStorageKey(profile), profileConfig)
persistedDashboardGridLayout = cloneDashboardGridLayout(layoutToPersist)
return queueDashboardProfileRemoteSave(getDashboardGridLayoutConfigKey(profile), profileConfig).catch(error =>
console.error(error),
)
}
// 将当前仪表板布局覆盖配置保存到本地和用户配置
// 持久化指定的仪表板布局覆盖配置。
function saveDashboardGridLayout(layout: DashboardGridLayoutConfig) {
return saveDashboardProfileConfig(layout)
}
// 克隆仪表板布局配置,避免临时编辑草稿和已确认布局共用对象引用。
function cloneDashboardGridLayout(layout: DashboardGridLayoutConfig): DashboardGridLayoutConfig {
return Object.entries(layout).reduce<DashboardGridLayoutConfig>((clonedLayout, [id, itemLayout]) => {
clonedLayout[id] = { ...itemLayout }
return clonedLayout
}, {})
}
// 获取仪表板组件的默认宽度,优先兼容插件旧版 cols.md / cols.cols 配置。
function getDefaultDashboardGridWidth(item: DashboardItem) {
const profile = dashboardLayoutProfile.value
@@ -818,10 +832,11 @@ function updateDashboardSettingsDialog() {
})
}
// 退出仪表板布局编辑模式;如果刚恢复默认布局,则跳过本次本地持久化。
// 退出仪表板布局编辑模式;用户点击确认时才把临时布局草稿持久化。
async function exitDashboardLayoutEditing() {
if (isDashboardGridLayoutResetPending.value) {
isDashboardGridLayoutResetPending.value = false
if (isDashboardGridLayoutResetDraft.value) {
await saveDashboardGridLayout(dashboardGridLayout.value)
isDashboardGridLayoutResetDraft.value = false
} else {
await persistCurrentDashboardGridLayout()
}
@@ -834,21 +849,21 @@ async function exitDashboardLayoutEditing() {
notifyDashboardContentResize()
}
// 清除用户本地布局覆盖并恢复内置组件和插件声明的默认占位,然后退出编辑模式
// 清除用户布局覆盖并恢复默认占位;编辑中仅写入临时草稿并等待确认持久化
async function resetDashboardGridLayout() {
const shouldPersistImmediately = !isLayoutEditing.value
dashboardGridLayout.value = {}
await saveDashboardGridLayout({})
dashboardGrid.value?.removeAll(false, false)
isDashboardGridLayoutResetPending.value = true
await syncDashboardGrid()
if (isLayoutEditing.value) {
await exitDashboardLayoutEditing()
} else {
await nextTick()
syncDashboardFillContentState()
resizeAutoDashboardItemsToContent()
notifyDashboardContentResize()
if (shouldPersistImmediately) {
await saveDashboardGridLayout({})
}
isDashboardGridLayoutResetDraft.value = isLayoutEditing.value
dashboardGrid.value?.removeAll(false, false)
await syncDashboardGrid()
await nextTick()
syncDashboardFillContentState()
resizeAutoDashboardItemsToContent()
notifyDashboardContentResize()
}
// 生成 appMode 底部动态按钮菜单,普通 Web 模式由页面内 FAB 承接。
@@ -893,14 +908,14 @@ useDynamicButton({
show: computed(() => appMode.value && route.path === '/dashboard'),
})
// 切换仪表板布局编辑模式,退出编辑时保存当前布局。
// 切换仪表板布局编辑模式,点击确认退出时持久化当前布局草稿
function toggleDashboardLayoutEditing() {
if (isLayoutEditing.value) {
void exitDashboardLayoutEditing()
return
}
isDashboardGridLayoutResetPending.value = false
isDashboardGridLayoutResetDraft.value = false
isLayoutEditing.value = true
nextTick(syncDashboardGrid)
}
@@ -920,7 +935,10 @@ async function loadDashboardConfig() {
// 设备档位配置同时承载 Grid 布局和显示项,显示项缺失时从旧版全局配置迁移。
const profileConfig = await loadDashboardProfileConfig(dashboardLayoutProfile.value)
const legacyEnable = profileConfig?.enabled === undefined ? await loadLegacyDashboardEnableConfig() : undefined
dashboardGridLayout.value = profileConfig?.items ?? {}
const loadedLayout = profileConfig?.items ?? {}
dashboardGridLayout.value = loadedLayout
persistedDashboardGridLayout = cloneDashboardGridLayout(loadedLayout)
isDashboardGridLayoutResetDraft.value = false
enableConfig.value = mergeDashboardEnableConfig(profileConfig?.enabled ?? legacyEnable)
if (profileConfig?.enabled === undefined && legacyEnable !== undefined) {
await saveDashboardProfileConfig()
@@ -1286,12 +1304,12 @@ function handleDashboardGridResize() {
notifyDashboardContentResize()
}
// 存用户拖动后的位置,并保持未手动调高组件继续按内容自适应。
// 存用户拖动后的位置,并保持未手动调高组件继续按内容自适应。
function handleDashboardGridDragStop() {
void persistCurrentDashboardGridLayout(false)
void cacheCurrentDashboardGridLayout(false)
}
// 存用户缩放后的布局,只有高度发生变化时才把高度标记为手动固定。
// 存用户缩放后的布局,只有高度发生变化时才把高度标记为手动固定。
function handleDashboardGridResizeStop(_event: Event, element: GridItemHTMLElement) {
const id = element.getAttribute('gs-id') ?? ''
const previousHeight = dashboardGridResizeStartHeights.get(id)
@@ -1301,7 +1319,7 @@ function handleDashboardGridResizeStop(_event: Event, element: GridItemHTMLEleme
dashboardGridResizeStartHeights.delete(id)
isDashboardGridResizing.value = false
notifyDashboardContentResize()
void persistCurrentDashboardGridLayout(heightChanged ? id : false)
void cacheCurrentDashboardGridLayout(heightChanged ? id : false)
}
// 合并连续 resize 通知,模拟浏览器窗口变化让组件内部内容自适配新尺寸。
@@ -1314,9 +1332,9 @@ function notifyDashboardContentResize() {
})
}
// 将 GridStack 保存结果归一化为本地布局覆盖表
async function persistDashboardGridLayout(manualHeightId: string | false = false) {
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return
// 将 GridStack 保存结果归一化为临时布局草稿
function cacheDashboardGridLayoutDraft(manualHeightId: string | false = false) {
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return undefined
const gridColumns = getCurrentDashboardGridColumns()
const savedWidgets = dashboardGrid.value.save(false, false, undefined, gridColumns)
@@ -1344,13 +1362,13 @@ async function persistDashboardGridLayout(manualHeightId: string | false = false
isPersistingDashboardGridLayoutFromGrid.value = true
dashboardGridLayout.value = nextLayout
const savePromise = saveDashboardGridLayout(nextLayout)
isDashboardGridLayoutResetDraft.value = false
nextTick(() => {
isPersistingDashboardGridLayoutFromGrid.value = false
resizeAutoDashboardItemsToContent()
})
await savePromise
return nextLayout
}
// 根据组件 ID 查找默认宽度,保存布局时用于兜底。
@@ -1360,13 +1378,20 @@ function getDefaultDashboardGridWidthById(id: string, maxColumns = DASHBOARD_GRI
return item ? Math.min(getDefaultDashboardGridWidth(item), maxColumns) : maxColumns
}
// 等待 GridStack 当前拖拽/缩放事件收尾后,保存用户当前看到的布局。
async function persistCurrentDashboardGridLayout(manualHeightId: string | false = false) {
// 等待 GridStack 当前拖拽/缩放事件收尾后,用户当前看到的布局写入临时草稿
async function cacheCurrentDashboardGridLayout(manualHeightId: string | false = false) {
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return
isDashboardGridLayoutResetPending.value = false
await nextTick()
await persistDashboardGridLayout(manualHeightId)
return cacheDashboardGridLayoutDraft(manualHeightId)
}
// 等待 GridStack 当前拖拽/缩放事件收尾后,持久化用户确认的临时布局草稿。
async function persistCurrentDashboardGridLayout(manualHeightId: string | false = false) {
const nextLayout = await cacheCurrentDashboardGridLayout(manualHeightId)
if (!nextLayout) return
await saveDashboardGridLayout(nextLayout)
}
// 清理 GridStack 内部响应式布局缓存,并用当前 Vue 布局状态重新注册已有 DOM 节点。
@@ -1408,7 +1433,10 @@ watch(
const legacyEnable = profileConfig?.enabled === undefined ? await loadLegacyDashboardEnableConfig() : undefined
if (profileSwitchId !== dashboardLayoutProfileSwitchId || dashboardLayoutProfile.value !== nextProfile) return
dashboardGridLayout.value = profileConfig?.items ?? {}
const loadedLayout = profileConfig?.items ?? {}
dashboardGridLayout.value = loadedLayout
persistedDashboardGridLayout = cloneDashboardGridLayout(loadedLayout)
isDashboardGridLayoutResetDraft.value = false
enableConfig.value = mergeDashboardEnableConfig(profileConfig?.enabled ?? legacyEnable)
if (profileConfig?.enabled === undefined && legacyEnable !== undefined) {
await saveDashboardProfileConfig()

View File

@@ -604,6 +604,11 @@ function getExistText(season: number) {
else return t('media.status.inLibrary')
}
// 判断指定季集是否已存在于媒体服务器
function isEpisodeExists(season: number, episode: number) {
return existsEpisodes.value[season]?.includes(episode) ?? false
}
// 计算订阅图标
const getSubscribeIcon = computed(() => {
if (mediaDetail.value.type === '电视剧') return subscribedSeasonNumbers.value.length > 0 ? 'mdi-heart' : 'mdi-heart-outline'
@@ -1036,7 +1041,14 @@ onUnmounted(() => {
:key="episode.episode_number"
class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
>
<div class="flex-1">
<div class="episode-info flex-1">
<VIcon
v-if="isEpisodeExists(season.season_number || 0, episode.episode_number || 0)"
color="success"
icon="mdi-check-circle"
class="episode-exists-badge"
size="small"
/>
<div class="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
<h3 class="text-lg">{{ episode.episode_number }} - {{ episode.name }}</h3>
<div class="flex items-center space-x-2">
@@ -1532,6 +1544,18 @@ a.crew-name {
min-inline-size: 0;
}
.episode-info {
position: relative;
min-inline-size: 0;
padding-inline-end: 2rem;
}
.episode-exists-badge {
position: absolute;
inset-block-start: 0.125rem;
inset-inline-end: 0;
}
.episode-group-label {
color: rgba(var(--v-theme-on-surface), 0.72);
font-size: 0.75rem;