mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 22:30:49 +08:00
refactor: new sns logic
This commit is contained in:
@@ -2,25 +2,41 @@
|
|||||||
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
||||||
<!-- 右侧朋友圈区域 -->
|
<!-- 右侧朋友圈区域 -->
|
||||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: #EDEDED">
|
<div class="flex-1 flex flex-col min-h-0" style="background-color: #EDEDED">
|
||||||
<div class="flex-1 overflow-auto min-h-0">
|
<div class="flex-1 overflow-auto min-h-0 bg-white" @scroll="onScroll">
|
||||||
<div class="max-w-2xl mx-auto px-4 py-4">
|
<div class="max-w-2xl mx-auto px-4 py-4">
|
||||||
|
<div class="relative w-full mb-12 -mt-4 bg-white">
|
||||||
|
<div class="h-64 w-full bg-[#333333] object-cover"></div>
|
||||||
|
|
||||||
|
<div class="absolute right-4 -bottom-6 flex items-end gap-4">
|
||||||
|
<div class="text-white font-bold text-xl mb-7 drop-shadow-md">
|
||||||
|
H3CoF6
|
||||||
|
</div>
|
||||||
|
<div class="w-[72px] h-[72px] rounded-lg bg-white p-[2px] shadow-sm">
|
||||||
|
<img
|
||||||
|
src="https://api.dicebear.com/7.x/shapes/svg?seed=H3CoF6"
|
||||||
|
class="w-full h-full rounded-md object-cover bg-gray-100"
|
||||||
|
alt="H3CoF6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="error" class="text-sm text-red-500 whitespace-pre-wrap py-2">{{ error }}</div>
|
<div v-if="error" class="text-sm text-red-500 whitespace-pre-wrap py-2">{{ error }}</div>
|
||||||
<div v-else-if="isLoading && posts.length === 0" class="text-sm text-gray-500 py-2">加载中…</div>
|
<div v-else-if="isLoading && posts.length === 0" class="text-sm text-gray-500 py-2">加载中…</div>
|
||||||
<div v-else-if="posts.length === 0" class="text-sm text-gray-500 py-2">暂无朋友圈数据</div>
|
<div v-else-if="posts.length === 0" class="text-sm text-gray-500 py-2">暂无朋友圈数据</div>
|
||||||
|
|
||||||
<!-- 图片匹配提示(实验功能) -->
|
<!-- 图片匹配提示(实验功能) -->
|
||||||
<div v-if="!error" class="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
<!-- <div v-if="!error" class="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">-->
|
||||||
<div class="font-medium">图片匹配(实验功能)</div>
|
<!-- <div class="font-medium">图片匹配(实验功能)</div>-->
|
||||||
<div class="mt-1 leading-5">
|
<!-- <div class="mt-1 leading-5">-->
|
||||||
图片可能会出现错配或无法显示。点击图片进入预览,可在“候选匹配”中手动选择;你的选择会保存在本机并在下次优先使用。
|
<!-- 图片可能会出现错配或无法显示。点击图片进入预览,可在“候选匹配”中手动选择;你的选择会保存在本机并在下次优先使用。-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<label class="mt-2 flex items-start gap-2 select-none">
|
<!-- <label class="mt-2 flex items-start gap-2 select-none">-->
|
||||||
<input v-model="snsAvoidOtherPicked" type="checkbox" class="mt-[2px]" />
|
<!-- <input v-model="snsAvoidOtherPicked" type="checkbox" class="mt-[2px]" />-->
|
||||||
<span class="leading-5">
|
<!-- <span class="leading-5">-->
|
||||||
自动匹配时,避开已被你手动指定到其他动态的图片(降低重复)
|
<!-- 自动匹配时,避开已被你手动指定到其他动态的图片(降低重复)-->
|
||||||
</span>
|
<!-- </span>-->
|
||||||
</label>
|
<!-- </label>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<div v-for="post in posts" :key="post.id" class="bg-white rounded-sm px-4 py-4 mb-3">
|
<div v-for="post in posts" :key="post.id" class="bg-white rounded-sm px-4 py-4 mb-3">
|
||||||
<div class="flex items-start gap-3" @contextmenu.prevent="openPostContextMenu($event, post)">
|
<div class="flex items-start gap-3" @contextmenu.prevent="openPostContextMenu($event, post)">
|
||||||
@@ -250,16 +266,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasMore" class="py-2">
|
<!-- <div v-if="hasMore" class="py-2">-->
|
||||||
<button
|
<!-- <button-->
|
||||||
type="button"
|
<!-- type="button"-->
|
||||||
class="w-full text-sm text-gray-600 py-2 rounded bg-white hover:bg-gray-50 border border-gray-200"
|
<!-- class="w-full text-sm text-gray-600 py-2 rounded bg-white hover:bg-gray-50 border border-gray-200"-->
|
||||||
:disabled="isLoading"
|
<!-- :disabled="isLoading"-->
|
||||||
@click="loadPosts({ reset: false })"
|
<!-- @click="loadPosts({ reset: false })"-->
|
||||||
>
|
<!-- >-->
|
||||||
{{ isLoading ? '加载中…' : '加载更多' }}
|
<!-- {{ isLoading ? '加载中…' : '加载更多' }}-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
<div v-if="isLoading && posts.length > 0" class="py-4 flex justify-center items-center">
|
||||||
|
<div class="w-5 h-5 border-2 border-gray-400 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hasMore && posts.length > 0" class="py-6 text-center text-xs text-gray-400">
|
||||||
|
—— 到底了 ——
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,67 +311,67 @@
|
|||||||
<img :src="previewSrc" alt="预览" class="max-w-[90vw] max-h-[70vh] object-contain" />
|
<img :src="previewSrc" alt="预览" class="max-w-[90vw] max-h-[70vh] object-contain" />
|
||||||
|
|
||||||
<!-- 候选匹配面板(仅在本地缓存匹配时有意义) -->
|
<!-- 候选匹配面板(仅在本地缓存匹配时有意义) -->
|
||||||
<div class="mt-3 w-full max-w-[90vw] rounded bg-black/35 text-white text-xs px-3 py-2">
|
<!-- <div class="mt-3 w-full max-w-[90vw] rounded bg-black/35 text-white text-xs px-3 py-2">-->
|
||||||
<div class="flex items-center justify-between gap-2">
|
<!-- <div class="flex items-center justify-between gap-2">-->
|
||||||
<div class="truncate">
|
<!-- <div class="truncate">-->
|
||||||
候选匹配:
|
<!-- 候选匹配:-->
|
||||||
<span v-if="previewCandidates.loading">加载中…</span>
|
<!-- <span v-if="previewCandidates.loading">加载中…</span>-->
|
||||||
<span v-else-if="previewCandidates.count > 0">共 {{ previewCandidates.count }} 个</span>
|
<!-- <span v-else-if="previewCandidates.count > 0">共 {{ previewCandidates.count }} 个</span>-->
|
||||||
<span v-else>未找到本地候选(可能仅能显示占位图)</span>
|
<!-- <span v-else>未找到本地候选(可能仅能显示占位图)</span>-->
|
||||||
<span v-if="previewEffectiveIdx != null" class="ml-2 text-white/80">当前:#{{ Number(previewEffectiveIdx) + 1 }}</span>
|
<!-- <span v-if="previewEffectiveIdx != null" class="ml-2 text-white/80">当前:#{{ Number(previewEffectiveIdx) + 1 }}</span>-->
|
||||||
<span v-if="previewHasUserOverride" class="ml-2 text-emerald-200">(已保存)</span>
|
<!-- <span v-if="previewHasUserOverride" class="ml-2 text-emerald-200">(已保存)</span>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="flex items-center gap-2 flex-shrink-0">
|
<!-- <div class="flex items-center gap-2 flex-shrink-0">-->
|
||||||
<button
|
<!-- <button-->
|
||||||
type="button"
|
<!-- type="button"-->
|
||||||
class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"
|
<!-- class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"-->
|
||||||
@click="toggleCandidatePanel"
|
<!-- @click="toggleCandidatePanel"-->
|
||||||
>
|
<!-- >-->
|
||||||
{{ previewCandidatesOpen ? '收起' : '展开' }}
|
<!-- {{ previewCandidatesOpen ? '收起' : '展开' }}-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
<button
|
<!-- <button-->
|
||||||
v-if="previewHasUserOverride"
|
<!-- v-if="previewHasUserOverride"-->
|
||||||
type="button"
|
<!-- type="button"-->
|
||||||
class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"
|
<!-- class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"-->
|
||||||
@click="clearUserOverrideForPreview"
|
<!-- @click="clearUserOverrideForPreview"-->
|
||||||
>
|
<!-- >-->
|
||||||
恢复自动
|
<!-- 恢复自动-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<div v-if="previewCandidates.error" class="mt-2 text-red-200 whitespace-pre-wrap">
|
<!-- <div v-if="previewCandidates.error" class="mt-2 text-red-200 whitespace-pre-wrap">-->
|
||||||
{{ previewCandidates.error }}
|
<!-- {{ previewCandidates.error }}-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<div v-if="previewCandidatesOpen && previewCandidates.count > 0" class="mt-2">
|
<!-- <div v-if="previewCandidatesOpen && previewCandidates.count > 0" class="mt-2">-->
|
||||||
<div class="flex gap-2 overflow-x-auto pb-1">
|
<!-- <div class="flex gap-2 overflow-x-auto pb-1">-->
|
||||||
<button
|
<!-- <button-->
|
||||||
v-for="cand in previewCandidates.items"
|
<!-- v-for="cand in previewCandidates.items"-->
|
||||||
:key="cand.idx"
|
<!-- :key="cand.idx"-->
|
||||||
type="button"
|
<!-- type="button"-->
|
||||||
class="flex-shrink-0 w-24"
|
<!-- class="flex-shrink-0 w-24"-->
|
||||||
@click="selectCandidateForPreview(cand.idx)"
|
<!-- @click="selectCandidateForPreview(cand.idx)"-->
|
||||||
>
|
<!-- >-->
|
||||||
<div class="w-24 h-24 rounded bg-black/20 overflow-hidden border border-white/10">
|
<!-- <div class="w-24 h-24 rounded bg-black/20 overflow-hidden border border-white/10">-->
|
||||||
<img :src="getPreviewCandidateSrc(cand.idx)" class="w-full h-full object-cover" alt="" />
|
<!-- <img :src="getPreviewCandidateSrc(cand.idx)" class="w-full h-full object-cover" alt="" />-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="mt-1 text-[11px] text-white/80">#{{ Number(cand.idx) + 1 }}</div>
|
<!-- <div class="mt-1 text-[11px] text-white/80">#{{ Number(cand.idx) + 1 }}</div>-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<div v-if="previewCandidates.hasMore" class="mt-2">
|
<!-- <div v-if="previewCandidates.hasMore" class="mt-2">-->
|
||||||
<button
|
<!-- <button-->
|
||||||
type="button"
|
<!-- type="button"-->
|
||||||
class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"
|
<!-- class="px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"-->
|
||||||
:disabled="previewCandidates.loadingMore"
|
<!-- :disabled="previewCandidates.loadingMore"-->
|
||||||
@click="loadMorePreviewCandidates"
|
<!-- @click="loadMorePreviewCandidates"-->
|
||||||
>
|
<!-- >-->
|
||||||
{{ previewCandidates.loadingMore ? '加载中…' : '加载更多候选' }}
|
<!-- {{ previewCandidates.loadingMore ? '加载中…' : '加载更多候选' }}-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -374,7 +396,7 @@ useHead({ title: '朋友圈 - 微信数据分析助手' })
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const chatAccounts = useChatAccountsStore()
|
const chatAccounts = useChatAccountsStore()
|
||||||
const { selectedAccount, accounts: availableAccounts } = storeToRefs(chatAccounts)
|
const { selectedAccount } = storeToRefs(chatAccounts)
|
||||||
|
|
||||||
const privacyStore = usePrivacyStore()
|
const privacyStore = usePrivacyStore()
|
||||||
const { privacyMode } = storeToRefs(privacyStore)
|
const { privacyMode } = storeToRefs(privacyStore)
|
||||||
@@ -389,108 +411,108 @@ const pageSize = 20
|
|||||||
const mediaBase = process.client ? 'http://localhost:8000' : ''
|
const mediaBase = process.client ? 'http://localhost:8000' : ''
|
||||||
|
|
||||||
// User overrides for SNS image matching (account-local, stored in localStorage).
|
// User overrides for SNS image matching (account-local, stored in localStorage).
|
||||||
const SNS_MEDIA_OVERRIDE_PREFIX = 'sns_media_override:v1:'
|
// const SNS_MEDIA_OVERRIDE_PREFIX = 'sns_media_override:v1:'
|
||||||
const SNS_MEDIA_OVERRIDE_REV_PREFIX = 'sns_media_override_rev:v1:'
|
// const SNS_MEDIA_OVERRIDE_REV_PREFIX = 'sns_media_override_rev:v1:'
|
||||||
const snsMediaOverrides = ref({})
|
// const snsMediaOverrides = ref({})
|
||||||
const snsMediaOverrideRev = ref('0')
|
// const snsMediaOverrideRev = ref('0')
|
||||||
|
|
||||||
const snsOverrideStorageKey = (account) => `${SNS_MEDIA_OVERRIDE_PREFIX}${String(account || '').trim()}`
|
// const snsOverrideStorageKey = (account) => `${SNS_MEDIA_OVERRIDE_PREFIX}${String(account || '').trim()}`
|
||||||
const snsOverrideRevStorageKey = (account) => `${SNS_MEDIA_OVERRIDE_REV_PREFIX}${String(account || '').trim()}`
|
// const snsOverrideRevStorageKey = (account) => `${SNS_MEDIA_OVERRIDE_REV_PREFIX}${String(account || '').trim()}`
|
||||||
const snsOverrideMediaKey = (postId, idx) => `${String(postId || '')}:${String(Number(idx) || 0)}`
|
// const snsOverrideMediaKey = (postId, idx) => `${String(postId || '')}:${String(Number(idx) || 0)}`
|
||||||
|
|
||||||
const loadSnsMediaOverrides = () => {
|
// const loadSnsMediaOverrides = () => {
|
||||||
if (!process.client) return
|
// if (!process.client) return
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!acc) {
|
// if (!acc) {
|
||||||
snsMediaOverrides.value = {}
|
// snsMediaOverrides.value = {}
|
||||||
snsMediaOverrideRev.value = '0'
|
// snsMediaOverrideRev.value = '0'
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const raw = localStorage.getItem(snsOverrideStorageKey(acc))
|
// const raw = localStorage.getItem(snsOverrideStorageKey(acc))
|
||||||
const parsed = raw ? JSON.parse(raw) : {}
|
// const parsed = raw ? JSON.parse(raw) : {}
|
||||||
snsMediaOverrides.value = parsed && typeof parsed === 'object' ? parsed : {}
|
// snsMediaOverrides.value = parsed && typeof parsed === 'object' ? parsed : {}
|
||||||
} catch {
|
// } catch {
|
||||||
snsMediaOverrides.value = {}
|
// snsMediaOverrides.value = {}
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const rev = localStorage.getItem(snsOverrideRevStorageKey(acc))
|
// const rev = localStorage.getItem(snsOverrideRevStorageKey(acc))
|
||||||
snsMediaOverrideRev.value = String(rev || '0')
|
// snsMediaOverrideRev.value = String(rev || '0')
|
||||||
} catch {
|
// } catch {
|
||||||
snsMediaOverrideRev.value = '0'
|
// snsMediaOverrideRev.value = '0'
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const saveSnsMediaOverrides = () => {
|
// const saveSnsMediaOverrides = () => {
|
||||||
if (!process.client) return
|
// if (!process.client) return
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!acc) return
|
// if (!acc) return
|
||||||
try {
|
// try {
|
||||||
localStorage.setItem(snsOverrideStorageKey(acc), JSON.stringify(snsMediaOverrides.value || {}))
|
// localStorage.setItem(snsOverrideStorageKey(acc), JSON.stringify(snsMediaOverrides.value || {}))
|
||||||
} catch {}
|
// } catch {}
|
||||||
try {
|
// try {
|
||||||
localStorage.setItem(snsOverrideRevStorageKey(acc), String(snsMediaOverrideRev.value || '0'))
|
// localStorage.setItem(snsOverrideRevStorageKey(acc), String(snsMediaOverrideRev.value || '0'))
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Settings: avoid auto-using an image that was manually pinned to another SNS post.
|
// Settings: avoid auto-using an image that was manually pinned to another SNS post.
|
||||||
const SNS_SNS_SETTINGS_PREFIX = 'sns_settings:v1:'
|
// const SNS_SNS_SETTINGS_PREFIX = 'sns_settings:v1:'
|
||||||
const snsAvoidOtherPicked = ref(true)
|
// const snsAvoidOtherPicked = ref(true)
|
||||||
const snsAvoidOtherPickedStorageKey = (account) => `${SNS_SNS_SETTINGS_PREFIX}${String(account || '').trim()}:avoid_other_picked`
|
// const snsAvoidOtherPickedStorageKey = (account) => `${SNS_SNS_SETTINGS_PREFIX}${String(account || '').trim()}:avoid_other_picked`
|
||||||
|
//
|
||||||
const loadSnsSettings = () => {
|
// const loadSnsSettings = () => {
|
||||||
if (!process.client) return
|
// if (!process.client) return
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!acc) return
|
// if (!acc) return
|
||||||
try {
|
// try {
|
||||||
const raw = localStorage.getItem(snsAvoidOtherPickedStorageKey(acc))
|
// const raw = localStorage.getItem(snsAvoidOtherPickedStorageKey(acc))
|
||||||
if (raw == null || raw === '') return
|
// if (raw == null || raw === '') return
|
||||||
snsAvoidOtherPicked.value = raw === '1' || raw === 'true'
|
// snsAvoidOtherPicked.value = raw === '1' || raw === 'true'
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const saveSnsSettings = () => {
|
// const saveSnsSettings = () => {
|
||||||
if (!process.client) return
|
// if (!process.client) return
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!acc) return
|
// if (!acc) return
|
||||||
try {
|
// try {
|
||||||
localStorage.setItem(snsAvoidOtherPickedStorageKey(acc), snsAvoidOtherPicked.value ? '1' : '0')
|
// localStorage.setItem(snsAvoidOtherPickedStorageKey(acc), snsAvoidOtherPicked.value ? '1' : '0')
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const syncSnsMediaPicksToBackend = async () => {
|
// const syncSnsMediaPicksToBackend = async () => {
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!acc) return
|
// if (!acc) return
|
||||||
try {
|
// try {
|
||||||
await api.saveSnsMediaPicks({ account: acc, picks: snsMediaOverrides.value || {} })
|
// await api.saveSnsMediaPicks({ account: acc, picks: snsMediaOverrides.value || {} })
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const getSnsMediaOverridePick = (postId, idx) => {
|
// const getSnsMediaOverridePick = (postId, idx) => {
|
||||||
const key = snsOverrideMediaKey(postId, idx)
|
// const key = snsOverrideMediaKey(postId, idx)
|
||||||
const v = snsMediaOverrides.value?.[key]
|
// const v = snsMediaOverrides.value?.[key]
|
||||||
return String(v || '').trim()
|
// return String(v || '').trim()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const setSnsMediaOverridePick = (postId, idx, pick) => {
|
// const setSnsMediaOverridePick = (postId, idx, pick) => {
|
||||||
if (!process.client) return
|
// if (!process.client) return
|
||||||
const key = snsOverrideMediaKey(postId, idx)
|
// const key = snsOverrideMediaKey(postId, idx)
|
||||||
const v = String(pick || '').trim()
|
// const v = String(pick || '').trim()
|
||||||
if (!v) {
|
// if (!v) {
|
||||||
if (snsMediaOverrides.value && Object.prototype.hasOwnProperty.call(snsMediaOverrides.value, key)) {
|
// if (snsMediaOverrides.value && Object.prototype.hasOwnProperty.call(snsMediaOverrides.value, key)) {
|
||||||
delete snsMediaOverrides.value[key]
|
// delete snsMediaOverrides.value[key]
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
snsMediaOverrides.value[key] = v
|
// snsMediaOverrides.value[key] = v
|
||||||
}
|
// }
|
||||||
saveSnsMediaOverrides()
|
// saveSnsMediaOverrides()
|
||||||
// Keep backend in sync so it can apply duplicate-avoidance logic.
|
// // Keep backend in sync so it can apply duplicate-avoidance logic.
|
||||||
// Then bump `pv` so other auto-matched images reload using the updated picks.
|
// // Then bump `pv` so other auto-matched images reload using the updated picks.
|
||||||
void syncSnsMediaPicksToBackend().finally(() => {
|
// void syncSnsMediaPicksToBackend().finally(() => {
|
||||||
snsMediaOverrideRev.value = String(Date.now())
|
// snsMediaOverrideRev.value = String(Date.now())
|
||||||
saveSnsMediaOverrides()
|
// saveSnsMediaOverrides()
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Track failed images per-post, per-index to render placeholders instead of broken <img>.
|
// Track failed images per-post, per-index to render placeholders instead of broken <img>.
|
||||||
const mediaErrors = ref({})
|
const mediaErrors = ref({})
|
||||||
@@ -600,6 +622,15 @@ const onCopyPostJsonClick = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onScroll = (e) => {
|
||||||
|
const { scrollTop, clientHeight, scrollHeight } = e.target
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 200) {
|
||||||
|
if (hasMore.value && !isLoading.value) {
|
||||||
|
loadPosts({ reset: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const postAvatarUrl = (username) => {
|
const postAvatarUrl = (username) => {
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
const acc = String(selectedAccount.value || '').trim()
|
||||||
const u = String(username || '').trim()
|
const u = String(username || '').trim()
|
||||||
@@ -690,7 +721,7 @@ const getSnsMediaUrl = (post, m, idx, rawUrl) => {
|
|||||||
const h = String(m?.size?.height || m?.size?.h || '').trim()
|
const h = String(m?.size?.height || m?.size?.h || '').trim()
|
||||||
const ts = String(m?.size?.totalSize || m?.size?.total_size || m?.size?.total || '').trim()
|
const ts = String(m?.size?.totalSize || m?.size?.total_size || m?.size?.total || '').trim()
|
||||||
const sizeIdx = mediaSizeGroupIndex(post, m, idx)
|
const sizeIdx = mediaSizeGroupIndex(post, m, idx)
|
||||||
const pick = getSnsMediaOverridePick(post?.id, idx)
|
// const pick = getSnsMediaOverridePick(post?.id, idx)
|
||||||
let md5 = normalizeHex32(m?.urlAttrs?.md5 || m?.thumbAttrs?.md5 || m?.urlAttrs?.MD5 || m?.thumbAttrs?.MD5)
|
let md5 = normalizeHex32(m?.urlAttrs?.md5 || m?.thumbAttrs?.md5 || m?.urlAttrs?.MD5 || m?.thumbAttrs?.MD5)
|
||||||
if (!md5) {
|
if (!md5) {
|
||||||
const match = /[?&]md5=([0-9a-fA-F]{16,32})/.exec(raw)
|
const match = /[?&]md5=([0-9a-fA-F]{16,32})/.exec(raw)
|
||||||
@@ -712,11 +743,11 @@ const getSnsMediaUrl = (post, m, idx, rawUrl) => {
|
|||||||
const mtype = String(m?.type || '').trim()
|
const mtype = String(m?.type || '').trim()
|
||||||
if (mtype) parts.set('media_type', mtype)
|
if (mtype) parts.set('media_type', mtype)
|
||||||
|
|
||||||
if (pick) parts.set('pick', pick)
|
// if (pick) parts.set('pick', pick)
|
||||||
if (!pick && snsAvoidOtherPicked.value) {
|
// if (!pick && snsAvoidOtherPicked.value) {
|
||||||
parts.set('avoid_picked', '1')
|
// parts.set('avoid_picked', '1')
|
||||||
parts.set('pv', String(snsMediaOverrideRev.value || '0'))
|
// parts.set('pv', String(snsMediaOverrideRev.value || '0'))
|
||||||
}
|
// }
|
||||||
if (md5) parts.set('md5', md5)
|
if (md5) parts.set('md5', md5)
|
||||||
// Bump this when changing backend matching logic to avoid stale cached wrong images.
|
// Bump this when changing backend matching logic to avoid stale cached wrong images.
|
||||||
parts.set('v', '7')
|
parts.set('v', '7')
|
||||||
@@ -789,52 +820,52 @@ const previewSrc = computed(() => {
|
|||||||
return getMediaPreviewSrc(ctx.post, ctx.media, ctx.idx)
|
return getMediaPreviewSrc(ctx.post, ctx.media, ctx.idx)
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewHasUserOverride = computed(() => {
|
// const previewHasUserOverride = computed(() => {
|
||||||
const ctx = previewCtx.value
|
// const ctx = previewCtx.value
|
||||||
if (!ctx) return false
|
// if (!ctx) return false
|
||||||
return !!getSnsMediaOverridePick(ctx.post?.id, ctx.idx)
|
// return !!getSnsMediaOverridePick(ctx.post?.id, ctx.idx)
|
||||||
})
|
// })
|
||||||
|
|
||||||
const previewEffectiveIdx = computed(() => {
|
// const previewEffectiveIdx = computed(() => {
|
||||||
const ctx = previewCtx.value
|
// const ctx = previewCtx.value
|
||||||
if (!ctx) return null
|
// if (!ctx) return null
|
||||||
const pick = getSnsMediaOverridePick(ctx.post?.id, ctx.idx)
|
// const pick = getSnsMediaOverridePick(ctx.post?.id, ctx.idx)
|
||||||
if (pick) {
|
// if (pick) {
|
||||||
const found = (previewCandidates.items || []).find((c) => String(c?.key || '') === pick)
|
// const found = (previewCandidates.items || []).find((c) => String(c?.key || '') === pick)
|
||||||
if (found) return Number(found.idx)
|
// if (found) return Number(found.idx)
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
const baseIdx = mediaSizeGroupIndex(ctx.post, ctx.media, ctx.idx)
|
// const baseIdx = mediaSizeGroupIndex(ctx.post, ctx.media, ctx.idx)
|
||||||
if (!snsAvoidOtherPicked.value) return baseIdx
|
// if (!snsAvoidOtherPicked.value) return baseIdx
|
||||||
const curPid = String(ctx.post?.id || '').trim()
|
// const curPid = String(ctx.post?.id || '').trim()
|
||||||
if (!curPid) return baseIdx
|
// if (!curPid) return baseIdx
|
||||||
|
//
|
||||||
|
// // Mirror backend logic: skip candidates that were manually pinned to other posts.
|
||||||
|
// const reserved = new Set()
|
||||||
|
// try {
|
||||||
|
// for (const [k, v] of Object.entries(snsMediaOverrides.value || {})) {
|
||||||
|
// const pid = String(k || '').split(':', 1)[0].trim()
|
||||||
|
// if (!pid || pid === curPid) continue
|
||||||
|
// const key = String(v || '').trim()
|
||||||
|
// if (key) reserved.add(key)
|
||||||
|
// }
|
||||||
|
// } catch {}
|
||||||
|
//
|
||||||
|
// const items = Array.isArray(previewCandidates.items) ? [...previewCandidates.items] : []
|
||||||
|
// items.sort((a, b) => Number(a?.idx || 0) - Number(b?.idx || 0))
|
||||||
|
// for (const c of items) {
|
||||||
|
// const i = Number(c?.idx)
|
||||||
|
// const key = String(c?.key || '').trim()
|
||||||
|
// if (!Number.isFinite(i) || i < baseIdx) continue
|
||||||
|
// if (!key) continue
|
||||||
|
// if (!reserved.has(key)) return i
|
||||||
|
// }
|
||||||
|
// return baseIdx
|
||||||
|
// })
|
||||||
|
|
||||||
// Mirror backend logic: skip candidates that were manually pinned to other posts.
|
// const toggleCandidatePanel = () => {
|
||||||
const reserved = new Set()
|
// previewCandidatesOpen.value = !previewCandidatesOpen.value
|
||||||
try {
|
// }
|
||||||
for (const [k, v] of Object.entries(snsMediaOverrides.value || {})) {
|
|
||||||
const pid = String(k || '').split(':', 1)[0].trim()
|
|
||||||
if (!pid || pid === curPid) continue
|
|
||||||
const key = String(v || '').trim()
|
|
||||||
if (key) reserved.add(key)
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const items = Array.isArray(previewCandidates.items) ? [...previewCandidates.items] : []
|
|
||||||
items.sort((a, b) => Number(a?.idx || 0) - Number(b?.idx || 0))
|
|
||||||
for (const c of items) {
|
|
||||||
const i = Number(c?.idx)
|
|
||||||
const key = String(c?.key || '').trim()
|
|
||||||
if (!Number.isFinite(i) || i < baseIdx) continue
|
|
||||||
if (!key) continue
|
|
||||||
if (!reserved.has(key)) return i
|
|
||||||
}
|
|
||||||
return baseIdx
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleCandidatePanel = () => {
|
|
||||||
previewCandidatesOpen.value = !previewCandidatesOpen.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPreviewCandidates = async ({ reset }) => {
|
const loadPreviewCandidates = async ({ reset }) => {
|
||||||
const ctx = previewCtx.value
|
const ctx = previewCtx.value
|
||||||
@@ -906,53 +937,53 @@ const closeImagePreview = () => {
|
|||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreviewCandidateSrc = (candIdx) => {
|
// const getPreviewCandidateSrc = (candIdx) => {
|
||||||
const ctx = previewCtx.value
|
// const ctx = previewCtx.value
|
||||||
const acc = String(selectedAccount.value || '').trim()
|
// const acc = String(selectedAccount.value || '').trim()
|
||||||
if (!ctx || !acc) return ''
|
// if (!ctx || !acc) return ''
|
||||||
|
//
|
||||||
|
// const idxNum = Number(candIdx)
|
||||||
|
// const cand = (previewCandidates.items || []).find((c) => Number(c?.idx) === idxNum)
|
||||||
|
// const key = String(cand?.key || '').trim()
|
||||||
|
// if (!key) return ''
|
||||||
|
//
|
||||||
|
// const parts = new URLSearchParams()
|
||||||
|
// parts.set('account', acc)
|
||||||
|
// parts.set('pick', key)
|
||||||
|
// const ct = String(ctx.post?.createTime || '').trim()
|
||||||
|
// if (ct) parts.set('create_time', ct)
|
||||||
|
// parts.set('v', '7')
|
||||||
|
// return `${mediaBase}/api/sns/media?${parts.toString()}`
|
||||||
|
// }
|
||||||
|
|
||||||
const idxNum = Number(candIdx)
|
// const selectCandidateForPreview = (candIdx) => {
|
||||||
const cand = (previewCandidates.items || []).find((c) => Number(c?.idx) === idxNum)
|
// const ctx = previewCtx.value
|
||||||
const key = String(cand?.key || '').trim()
|
// if (!ctx) return
|
||||||
if (!key) return ''
|
// const idxNum = Number(candIdx)
|
||||||
|
// const cand = (previewCandidates.items || []).find((c) => Number(c?.idx) === idxNum)
|
||||||
const parts = new URLSearchParams()
|
// const key = String(cand?.key || '').trim()
|
||||||
parts.set('account', acc)
|
// if (!key) return
|
||||||
parts.set('pick', key)
|
// setSnsMediaOverridePick(ctx.post?.id, ctx.idx, key)
|
||||||
const ct = String(ctx.post?.createTime || '').trim()
|
// // Allow <img> to retry after user switches candidates.
|
||||||
if (ct) parts.set('create_time', ct)
|
// try {
|
||||||
parts.set('v', '7')
|
// delete mediaErrors.value[mediaErrorKey(ctx.post?.id, ctx.idx)]
|
||||||
return `${mediaBase}/api/sns/media?${parts.toString()}`
|
// } catch {}
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const selectCandidateForPreview = (candIdx) => {
|
// const clearUserOverrideForPreview = () => {
|
||||||
const ctx = previewCtx.value
|
// const ctx = previewCtx.value
|
||||||
if (!ctx) return
|
// if (!ctx) return
|
||||||
const idxNum = Number(candIdx)
|
// setSnsMediaOverridePick(ctx.post?.id, ctx.idx, '')
|
||||||
const cand = (previewCandidates.items || []).find((c) => Number(c?.idx) === idxNum)
|
// try {
|
||||||
const key = String(cand?.key || '').trim()
|
// delete mediaErrors.value[mediaErrorKey(ctx.post?.id, ctx.idx)]
|
||||||
if (!key) return
|
// } catch {}
|
||||||
setSnsMediaOverridePick(ctx.post?.id, ctx.idx, key)
|
// }
|
||||||
// Allow <img> to retry after user switches candidates.
|
//
|
||||||
try {
|
// const loadMorePreviewCandidates = async () => {
|
||||||
delete mediaErrors.value[mediaErrorKey(ctx.post?.id, ctx.idx)]
|
// if (previewCandidates.loading || previewCandidates.loadingMore) return
|
||||||
} catch {}
|
// if (!previewCandidates.hasMore) return
|
||||||
}
|
// await loadPreviewCandidates({ reset: false })
|
||||||
|
// }
|
||||||
const clearUserOverrideForPreview = () => {
|
|
||||||
const ctx = previewCtx.value
|
|
||||||
if (!ctx) return
|
|
||||||
setSnsMediaOverridePick(ctx.post?.id, ctx.idx, '')
|
|
||||||
try {
|
|
||||||
delete mediaErrors.value[mediaErrorKey(ctx.post?.id, ctx.idx)]
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMorePreviewCandidates = async () => {
|
|
||||||
if (previewCandidates.loading || previewCandidates.loadingMore) return
|
|
||||||
if (!previewCandidates.hasMore) return
|
|
||||||
await loadPreviewCandidates({ reset: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMediaClick = (post, m, idx = 0) => {
|
const onMediaClick = (post, m, idx = 0) => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
@@ -1029,35 +1060,52 @@ const loadPosts = async ({ reset }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
// watch(
|
||||||
() => selectedAccount.value,
|
// () => selectedAccount.value,
|
||||||
async (v, oldV) => {
|
// async (v, oldV) => {
|
||||||
if (v && v !== oldV) {
|
// if (v && v !== oldV) {
|
||||||
// Account switch: reload overrides and reset preview state.
|
// // Account switch: reload overrides and reset preview state.
|
||||||
loadSnsMediaOverrides()
|
// loadSnsMediaOverrides()
|
||||||
loadSnsSettings()
|
// loadSnsSettings()
|
||||||
void syncSnsMediaPicksToBackend()
|
// void syncSnsMediaPicksToBackend()
|
||||||
if (previewCtx.value) closeImagePreview()
|
// if (previewCtx.value) closeImagePreview()
|
||||||
await loadPosts({ reset: true })
|
// await loadPosts({ reset: true })
|
||||||
} else if (!v) {
|
// } else if (!v) {
|
||||||
snsMediaOverrides.value = {}
|
// snsMediaOverrides.value = {}
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
|
|
||||||
|
// watch(
|
||||||
|
// () => snsAvoidOtherPicked.value,
|
||||||
|
// () => {
|
||||||
|
// saveSnsSettings()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// onMounted(async () => {
|
||||||
|
// privacyStore.init()
|
||||||
|
// await loadAccounts()
|
||||||
|
// loadSnsMediaOverrides()
|
||||||
|
// loadSnsSettings()
|
||||||
|
// void syncSnsMediaPicksToBackend()
|
||||||
|
// })
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => snsAvoidOtherPicked.value,
|
() => selectedAccount.value,
|
||||||
() => {
|
async (v, oldV) => {
|
||||||
saveSnsSettings()
|
if (v && v !== oldV) {
|
||||||
}
|
if (previewCtx.value) closeImagePreview()
|
||||||
|
await loadPosts({ reset: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
privacyStore.init()
|
privacyStore.init()
|
||||||
await loadAccounts()
|
await loadAccounts()
|
||||||
loadSnsMediaOverrides()
|
|
||||||
loadSnsSettings()
|
|
||||||
void syncSnsMediaPicksToBackend()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const onGlobalClick = () => {
|
const onGlobalClick = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user