diff --git a/app/pages/me/media/index.vue b/app/pages/me/media/index.vue index 14e399d..a27f801 100644 --- a/app/pages/me/media/index.vue +++ b/app/pages/me/media/index.vue @@ -42,6 +42,7 @@ function resolveCopyOrigin(): string { const page = ref(1) const pageSize = ref(20) +const cardDensity = ref<'compact' | 'detailed'>('compact') const items = ref([]) const total = ref(0) const loading = ref(true) @@ -90,6 +91,8 @@ const pageSizeItems = [ { label: '每页 50', value: 50 }, ] +const isCompact = computed(() => cardDensity.value === 'compact') + function formatBytes(n: number): string { if (n < 1024) { return `${n} B` @@ -420,22 +423,48 @@ async function copyMarkdown(item: MediaAssetRow) { + +
+ + 简洁 + + + 复杂 + +
+
-
-
+
+
@@ -456,12 +485,16 @@ async function copyMarkdown(item: MediaAssetRow) { />
- -
-
+ +
+
{{ item.storageKey }}
-
+
{{ formatBytes(item.sizeBytes) }} {{ formatDt(item.createdAt) }}
-

+

引用数: {{ item.refCount }}

-
+
-
+
文件描述
@@ -578,7 +611,7 @@ async function copyMarkdown(item: MediaAssetRow) {

{{ deleteDisabledHint(item) }} diff --git a/app/pages/me/rss/index.vue b/app/pages/me/rss/index.vue index 753031f..8bc154c 100644 --- a/app/pages/me/rss/index.vue +++ b/app/pages/me/rss/index.vue @@ -27,6 +27,8 @@ const feedUrl = ref('') const loading = ref(true) const selectedFeedId = ref(null) const copiedItemId = ref(null) +const readingMode = ref(false) +const activeItemId = ref(null) let copyResetTimer: ReturnType | undefined const { user, refresh } = useAuthSession() @@ -48,6 +50,23 @@ const selectedFeedLabel = computed(() => { return f?.title || f?.feedUrl || '当前订阅' }) +const activeItemIndex = computed(() => { + if (activeItemId.value === null) { + return -1 + } + return filteredItems.value.findIndex((it) => it.id === activeItemId.value) +}) + +const activeItem = computed(() => { + if (activeItemIndex.value < 0) { + return null + } + return filteredItems.value[activeItemIndex.value] ?? null +}) + +const hasPrevItem = computed(() => activeItemIndex.value > 0) +const hasNextItem = computed(() => activeItemIndex.value >= 0 && activeItemIndex.value < filteredItems.value.length - 1) + function formatNextSync(iso: string) { return new Date(iso).toLocaleString('zh-CN', { dateStyle: 'short', timeStyle: 'short' }) } @@ -71,6 +90,11 @@ function countForFeed(feedId: number) { return items.value.filter((it) => it.feedId === feedId).length } +function feedLabelFor(feedId: number) { + const f = feeds.value.find((x) => x.id === feedId) + return f?.title || f?.feedUrl || '未知来源' +} + function unlistedSharePath(publicSlug: string, token: string) { return `/p/${encodeURIComponent(publicSlug)}/t/${encodeURIComponent(token)}` } @@ -127,9 +151,25 @@ async function removeFeed(id: number) { } async function setItemVis(id: number, visibility: string) { - await fetchData(`/api/me/rss/items/${id}`, { method: 'PATCH', body: { visibility } }) - await load() - toast.add({ title: '可见性已更新', color: 'success' }) + const idx = items.value.findIndex((it) => it.id === id) + if (idx < 0) { + return + } + + const prev = items.value[idx].visibility + if (prev === visibility) { + return + } + + // 乐观更新:避免整页重新加载导致的视觉抖动和滚动跳动 + items.value[idx].visibility = visibility + try { + await fetchData(`/api/me/rss/items/${id}`, { method: 'PATCH', body: { visibility } }) + toast.add({ title: '可见性已更新', color: 'success' }) + } catch { + items.value[idx].visibility = prev + toast.add({ title: '更新失败,已恢复原状态', color: 'error' }) + } } async function copyUnlistedLink(it: Item) { @@ -151,85 +191,279 @@ async function copyUnlistedLink(it: Item) { // ignore } } + +function openReadingMode() { + readingMode.value = true + if (!filteredItems.value.length) { + activeItemId.value = null + return + } + if (!filteredItems.value.some((it) => it.id === activeItemId.value)) { + activeItemId.value = filteredItems.value[0].id + } +} + +function closeReadingMode() { + readingMode.value = false +} + +function selectPrevItem() { + if (!hasPrevItem.value) { + return + } + const prev = filteredItems.value[activeItemIndex.value - 1] + if (prev) { + activeItemId.value = prev.id + } +} + +function selectNextItem() { + if (!hasNextItem.value) { + return + } + const next = filteredItems.value[activeItemIndex.value + 1] + if (next) { + activeItemId.value = next.id + } +} + +watch(filteredItems, (list) => { + if (!list.length) { + activeItemId.value = null + return + } + if (!list.some((it) => it.id === activeItemId.value)) { + activeItemId.value = list[0].id + } +}, { immediate: true })