update at 2026-02-07 15:11:16
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user