update at 2026-02-07 15:11:16

This commit is contained in:
douboer
2026-02-07 15:11:16 +08:00
parent d5cfaa4605
commit 0f7ade6945

View File

@@ -11,35 +11,51 @@ const uiStore = useUiStore()
const previewItems = ref<PreviewItemType[]>([])
const isGenerating = ref(false)
const isBatchGenerating = ref(false)
const activePreviewFonts = ref<FontInfo[]>([])
const processedFontCount = ref(0)
const renderedPreviewCount = ref(0)
const previewTriggerItemEl = ref<HTMLElement | null>(null)
const previewFonts = computed(() => fontStore.previewFonts)
const inputText = computed(() => uiStore.inputText)
const fontSize = computed(() => uiStore.fontSize)
const fillColor = computed(() => uiStore.textColor)
const PREVIEW_DEBOUNCE_MS = 240
const PREVIEW_CONCURRENCY = 4
const PREVIEW_GEOMETRY_CACHE_LIMIT = 600
const PREVIEW_COLOR_TOKEN = '__FONT2SVG_FILL__'
const PREVIEW_BATCH_SIZE = 20
const PREVIEW_PREFETCH_OFFSET = 10
let previewGenerateTimer: ReturnType<typeof setTimeout> | null = null
let previewGenerationToken = 0
let hasTriggeredInitialGenerate = false
let previewLazyLoadObserver: IntersectionObserver | null = null
const previewGeometryCache = new Map<string, Omit<SvgGenerateResult, 'svg'> & { svgTemplate: string }>()
const renderedPreviewCount = ref(0)
const previewTriggerItemEl = ref<HTMLElement | null>(null)
const isAllPreviewSelected = computed(() => {
return previewItems.value.length > 0 && previewItems.value.every(item => item.selected)
})
const hasMorePreviewItems = computed(() => renderedPreviewCount.value < previewItems.value.length)
const visiblePreviewItems = computed(() => previewItems.value.slice(0, renderedPreviewCount.value))
const hasMorePreviewItems = computed(() => {
return (
renderedPreviewCount.value < previewItems.value.length ||
processedFontCount.value < activePreviewFonts.value.length
)
})
const visiblePreviewItems = computed(() => {
return previewItems.value.slice(0, renderedPreviewCount.value)
})
const previewTriggerIndex = computed(() => {
if (!hasMorePreviewItems.value || renderedPreviewCount.value <= 0) {
if (!hasMorePreviewItems.value || visiblePreviewItems.value.length <= 0) {
return -1
}
return Math.max(0, renderedPreviewCount.value - PREVIEW_PREFETCH_OFFSET)
return Math.max(0, visiblePreviewItems.value.length - PREVIEW_PREFETCH_OFFSET)
})
function isStaleGeneration(token: number): boolean {
@@ -53,13 +69,13 @@ function scheduleGeneratePreviews(withDebounce = true) {
}
if (!withDebounce) {
void generatePreviews()
void regeneratePreviews()
return
}
previewGenerateTimer = setTimeout(() => {
previewGenerateTimer = null
void generatePreviews()
void regeneratePreviews()
}, PREVIEW_DEBOUNCE_MS)
}
@@ -84,16 +100,6 @@ function disconnectPreviewLazyLoadObserver() {
previewLazyLoadObserver = null
}
function loadNextPreviewBatch() {
if (!hasMorePreviewItems.value) {
return
}
renderedPreviewCount.value = Math.min(
renderedPreviewCount.value + PREVIEW_BATCH_SIZE,
previewItems.value.length,
)
}
function bindPreviewLazyLoadObserver() {
disconnectPreviewLazyLoadObserver()
@@ -106,7 +112,7 @@ function bindPreviewLazyLoadObserver() {
(entries) => {
const isVisible = entries.some(entry => entry.isIntersecting)
if (isVisible) {
loadNextPreviewBatch()
void handleLoadMoreByScroll()
}
},
{
@@ -126,12 +132,7 @@ function setPreviewItemRef(el: unknown, index: number) {
}
function getPreviewGeometryCacheKey(fontInfo: FontInfo): string {
return [
fontInfo.id,
fontInfo.path,
inputText.value,
fontSize.value,
].join('::')
return [fontInfo.id, fontInfo.path, inputText.value, fontSize.value].join('::')
}
function readPreviewGeometryFromCache(key: string) {
@@ -140,7 +141,6 @@ function readPreviewGeometryFromCache(key: string) {
return null
}
// LRU: 读取后刷新顺序
previewGeometryCache.delete(key)
previewGeometryCache.set(key, cached)
return cached
@@ -185,6 +185,7 @@ async function generateSvgWithPreviewCache(fontInfo: FontInfo): Promise<SvgGener
fontName: baseResult.fontName,
svgTemplate: baseResult.svg,
}
writePreviewGeometryToCache(cacheKey, geometry)
}
@@ -196,68 +197,21 @@ async function generateSvgWithPreviewCache(fontInfo: FontInfo): Promise<SvgGener
}
}
watch(
[previewFonts, inputText, fontSize, fillColor],
() => {
scheduleGeneratePreviews(hasTriggeredInitialGenerate)
hasTriggeredInitialGenerate = true
},
{ immediate: true }
)
watch(
previewItems,
() => {
renderedPreviewCount.value = Math.min(PREVIEW_BATCH_SIZE, previewItems.value.length)
},
{ immediate: true },
)
watch(
[previewTriggerIndex, hasMorePreviewItems],
async () => {
previewTriggerItemEl.value = null
await nextTick()
bindPreviewLazyLoadObserver()
},
{ immediate: true },
)
onBeforeUnmount(() => {
if (previewGenerateTimer !== null) {
clearTimeout(previewGenerateTimer)
previewGenerateTimer = null
}
disconnectPreviewLazyLoadObserver()
previewGenerationToken += 1
})
async function generatePreviews() {
const generationToken = ++previewGenerationToken
const validPreviewFontIds = new Set(previewFonts.value.map(font => font.id))
uiStore.retainExportItemsByFontIds(validPreviewFontIds)
if (!inputText.value || inputText.value.trim() === '') {
if (!isStaleGeneration(generationToken)) {
previewItems.value = []
}
return
async function generatePreviewBatch(
fonts: FontInfo[],
startIndex: number,
batchSize: number,
generationToken: number,
): Promise<PreviewItemType[]> {
const endIndex = Math.min(startIndex + batchSize, fonts.length)
const batchFonts = fonts.slice(startIndex, endIndex)
if (batchFonts.length === 0) {
return []
}
const fonts = previewFonts.value
if (fonts.length === 0) {
if (!isStaleGeneration(generationToken)) {
previewItems.value = []
}
return
}
isGenerating.value = true
try {
const selectedFontIdSet = new Set(uiStore.selectedExportItems.map(item => item.fontInfo.id))
const items = new Array<PreviewItemType | null>(fonts.length).fill(null)
const workerCount = Math.min(PREVIEW_CONCURRENCY, fonts.length)
const items = new Array<PreviewItemType | null>(batchFonts.length).fill(null)
const workerCount = Math.min(PREVIEW_CONCURRENCY, batchFonts.length)
let nextIndex = 0
const worker = async () => {
@@ -266,13 +220,13 @@ async function generatePreviews() {
return
}
const currentIndex = nextIndex
const localIndex = nextIndex
nextIndex += 1
if (currentIndex >= fonts.length) {
if (localIndex >= batchFonts.length) {
return
}
const fontInfo = fonts[currentIndex]
const fontInfo = batchFonts[localIndex]
if (!fontInfo) {
continue
}
@@ -300,7 +254,7 @@ async function generatePreviews() {
return
}
items[currentIndex] = {
items[localIndex] = {
fontInfo,
svgResult,
selected: selectedFontIdSet.has(fontInfo.id),
@@ -314,12 +268,97 @@ async function generatePreviews() {
await Promise.all(Array.from({ length: workerCount }, () => worker()))
if (isStaleGeneration(generationToken)) {
return []
}
return items.filter((item): item is PreviewItemType => item !== null)
}
async function loadNextPreviewBatch(generationToken: number) {
if (isBatchGenerating.value || isStaleGeneration(generationToken)) {
return
}
previewItems.value = items.filter((item): item is PreviewItemType => item !== null)
const startIndex = processedFontCount.value
if (startIndex >= activePreviewFonts.value.length) {
return
}
isBatchGenerating.value = true
try {
const batchItems = await generatePreviewBatch(
activePreviewFonts.value,
startIndex,
PREVIEW_BATCH_SIZE,
generationToken,
)
if (isStaleGeneration(generationToken)) {
return
}
processedFontCount.value = Math.min(
startIndex + PREVIEW_BATCH_SIZE,
activePreviewFonts.value.length,
)
previewItems.value = [...previewItems.value, ...batchItems]
renderedPreviewCount.value = Math.min(
renderedPreviewCount.value + PREVIEW_BATCH_SIZE,
previewItems.value.length,
)
} catch (error) {
console.error('Failed to generate previews:', error)
console.error('Failed to load preview batch:', error)
} finally {
if (!isStaleGeneration(generationToken)) {
isBatchGenerating.value = false
}
}
}
async function handleLoadMoreByScroll() {
const generationToken = previewGenerationToken
if (isBatchGenerating.value || isStaleGeneration(generationToken)) {
return
}
if (renderedPreviewCount.value < previewItems.value.length) {
renderedPreviewCount.value = Math.min(
renderedPreviewCount.value + PREVIEW_BATCH_SIZE,
previewItems.value.length,
)
return
}
if (processedFontCount.value < activePreviewFonts.value.length) {
await loadNextPreviewBatch(generationToken)
}
}
async function regeneratePreviews() {
const generationToken = ++previewGenerationToken
const nextPreviewFonts = [...previewFonts.value]
const validPreviewFontIds = new Set(nextPreviewFonts.map(font => font.id))
uiStore.retainExportItemsByFontIds(validPreviewFontIds)
activePreviewFonts.value = nextPreviewFonts
processedFontCount.value = 0
previewItems.value = []
renderedPreviewCount.value = 0
if (!inputText.value || inputText.value.trim() === '' || nextPreviewFonts.length === 0) {
isGenerating.value = false
return
}
isGenerating.value = true
try {
await loadNextPreviewBatch(generationToken)
} catch (error) {
console.error('Failed to regenerate previews:', error)
} finally {
if (!isStaleGeneration(generationToken)) {
isGenerating.value = false
@@ -327,6 +366,34 @@ async function generatePreviews() {
}
}
watch(
[previewFonts, inputText, fontSize, fillColor],
() => {
scheduleGeneratePreviews(hasTriggeredInitialGenerate)
hasTriggeredInitialGenerate = true
},
{ immediate: true },
)
watch(
[previewTriggerIndex, hasMorePreviewItems],
async () => {
previewTriggerItemEl.value = null
await nextTick()
bindPreviewLazyLoadObserver()
},
{ immediate: true },
)
onBeforeUnmount(() => {
if (previewGenerateTimer !== null) {
clearTimeout(previewGenerateTimer)
previewGenerateTimer = null
}
disconnectPreviewLazyLoadObserver()
previewGenerationToken += 1
})
function toggleSelectItem(item: PreviewItemType) {
item.selected = !item.selected
uiStore.toggleExportItem(item)
@@ -396,7 +463,7 @@ defineExpose({
</div>
<div v-if="hasMorePreviewItems" class="text-xs text-[#86909c] text-center py-2">
继续下滑加载更多{{ visiblePreviewItems.length }}/{{ previewItems.length }}
{{ isBatchGenerating ? '加载中...' : `继续下滑加载更多${visiblePreviewItems.length}/${activePreviewFonts.length}` }}
</div>
</div>
</div>