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 previewItems = ref<PreviewItemType[]>([])
|
||||||
const isGenerating = ref(false)
|
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 previewFonts = computed(() => fontStore.previewFonts)
|
||||||
const inputText = computed(() => uiStore.inputText)
|
const inputText = computed(() => uiStore.inputText)
|
||||||
const fontSize = computed(() => uiStore.fontSize)
|
const fontSize = computed(() => uiStore.fontSize)
|
||||||
const fillColor = computed(() => uiStore.textColor)
|
const fillColor = computed(() => uiStore.textColor)
|
||||||
|
|
||||||
const PREVIEW_DEBOUNCE_MS = 240
|
const PREVIEW_DEBOUNCE_MS = 240
|
||||||
const PREVIEW_CONCURRENCY = 4
|
const PREVIEW_CONCURRENCY = 4
|
||||||
const PREVIEW_GEOMETRY_CACHE_LIMIT = 600
|
const PREVIEW_GEOMETRY_CACHE_LIMIT = 600
|
||||||
const PREVIEW_COLOR_TOKEN = '__FONT2SVG_FILL__'
|
const PREVIEW_COLOR_TOKEN = '__FONT2SVG_FILL__'
|
||||||
const PREVIEW_BATCH_SIZE = 20
|
const PREVIEW_BATCH_SIZE = 20
|
||||||
const PREVIEW_PREFETCH_OFFSET = 10
|
const PREVIEW_PREFETCH_OFFSET = 10
|
||||||
|
|
||||||
let previewGenerateTimer: ReturnType<typeof setTimeout> | null = null
|
let previewGenerateTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let previewGenerationToken = 0
|
let previewGenerationToken = 0
|
||||||
let hasTriggeredInitialGenerate = false
|
let hasTriggeredInitialGenerate = false
|
||||||
let previewLazyLoadObserver: IntersectionObserver | null = null
|
let previewLazyLoadObserver: IntersectionObserver | null = null
|
||||||
|
|
||||||
const previewGeometryCache = new Map<string, Omit<SvgGenerateResult, 'svg'> & { svgTemplate: string }>()
|
const previewGeometryCache = new Map<string, Omit<SvgGenerateResult, 'svg'> & { svgTemplate: string }>()
|
||||||
const renderedPreviewCount = ref(0)
|
|
||||||
const previewTriggerItemEl = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const isAllPreviewSelected = computed(() => {
|
const isAllPreviewSelected = computed(() => {
|
||||||
return previewItems.value.length > 0 && previewItems.value.every(item => item.selected)
|
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(() => {
|
const previewTriggerIndex = computed(() => {
|
||||||
if (!hasMorePreviewItems.value || renderedPreviewCount.value <= 0) {
|
if (!hasMorePreviewItems.value || visiblePreviewItems.value.length <= 0) {
|
||||||
return -1
|
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 {
|
function isStaleGeneration(token: number): boolean {
|
||||||
@@ -53,13 +69,13 @@ function scheduleGeneratePreviews(withDebounce = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!withDebounce) {
|
if (!withDebounce) {
|
||||||
void generatePreviews()
|
void regeneratePreviews()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
previewGenerateTimer = setTimeout(() => {
|
previewGenerateTimer = setTimeout(() => {
|
||||||
previewGenerateTimer = null
|
previewGenerateTimer = null
|
||||||
void generatePreviews()
|
void regeneratePreviews()
|
||||||
}, PREVIEW_DEBOUNCE_MS)
|
}, PREVIEW_DEBOUNCE_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,16 +100,6 @@ function disconnectPreviewLazyLoadObserver() {
|
|||||||
previewLazyLoadObserver = null
|
previewLazyLoadObserver = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNextPreviewBatch() {
|
|
||||||
if (!hasMorePreviewItems.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
renderedPreviewCount.value = Math.min(
|
|
||||||
renderedPreviewCount.value + PREVIEW_BATCH_SIZE,
|
|
||||||
previewItems.value.length,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindPreviewLazyLoadObserver() {
|
function bindPreviewLazyLoadObserver() {
|
||||||
disconnectPreviewLazyLoadObserver()
|
disconnectPreviewLazyLoadObserver()
|
||||||
|
|
||||||
@@ -106,7 +112,7 @@ function bindPreviewLazyLoadObserver() {
|
|||||||
(entries) => {
|
(entries) => {
|
||||||
const isVisible = entries.some(entry => entry.isIntersecting)
|
const isVisible = entries.some(entry => entry.isIntersecting)
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
loadNextPreviewBatch()
|
void handleLoadMoreByScroll()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -126,12 +132,7 @@ function setPreviewItemRef(el: unknown, index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPreviewGeometryCacheKey(fontInfo: FontInfo): string {
|
function getPreviewGeometryCacheKey(fontInfo: FontInfo): string {
|
||||||
return [
|
return [fontInfo.id, fontInfo.path, inputText.value, fontSize.value].join('::')
|
||||||
fontInfo.id,
|
|
||||||
fontInfo.path,
|
|
||||||
inputText.value,
|
|
||||||
fontSize.value,
|
|
||||||
].join('::')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPreviewGeometryFromCache(key: string) {
|
function readPreviewGeometryFromCache(key: string) {
|
||||||
@@ -140,7 +141,6 @@ function readPreviewGeometryFromCache(key: string) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// LRU: 读取后刷新顺序
|
|
||||||
previewGeometryCache.delete(key)
|
previewGeometryCache.delete(key)
|
||||||
previewGeometryCache.set(key, cached)
|
previewGeometryCache.set(key, cached)
|
||||||
return cached
|
return cached
|
||||||
@@ -185,6 +185,7 @@ async function generateSvgWithPreviewCache(fontInfo: FontInfo): Promise<SvgGener
|
|||||||
fontName: baseResult.fontName,
|
fontName: baseResult.fontName,
|
||||||
svgTemplate: baseResult.svg,
|
svgTemplate: baseResult.svg,
|
||||||
}
|
}
|
||||||
|
|
||||||
writePreviewGeometryToCache(cacheKey, geometry)
|
writePreviewGeometryToCache(cacheKey, geometry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,20 +197,181 @@ async function generateSvgWithPreviewCache(fontInfo: FontInfo): Promise<SvgGener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 selectedFontIdSet = new Set(uiStore.selectedExportItems.map(item => item.fontInfo.id))
|
||||||
|
const items = new Array<PreviewItemType | null>(batchFonts.length).fill(null)
|
||||||
|
const workerCount = Math.min(PREVIEW_CONCURRENCY, batchFonts.length)
|
||||||
|
let nextIndex = 0
|
||||||
|
|
||||||
|
const worker = async () => {
|
||||||
|
while (true) {
|
||||||
|
if (isStaleGeneration(generationToken)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const localIndex = nextIndex
|
||||||
|
nextIndex += 1
|
||||||
|
if (localIndex >= batchFonts.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontInfo = batchFonts[localIndex]
|
||||||
|
if (!fontInfo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fontInfo.loaded) {
|
||||||
|
try {
|
||||||
|
await fontStore.loadFont(fontInfo)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load font ${fontInfo.name}:`, error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStaleGeneration(generationToken) || !fontInfo.font) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const svgResult = await generateSvgWithPreviewCache(fontInfo)
|
||||||
|
if (!svgResult) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStaleGeneration(generationToken)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items[localIndex] = {
|
||||||
|
fontInfo,
|
||||||
|
svgResult,
|
||||||
|
selected: selectedFontIdSet.has(fontInfo.id),
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to generate SVG for ${fontInfo.name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[previewFonts, inputText, fontSize, fillColor],
|
[previewFonts, inputText, fontSize, fillColor],
|
||||||
() => {
|
() => {
|
||||||
scheduleGeneratePreviews(hasTriggeredInitialGenerate)
|
scheduleGeneratePreviews(hasTriggeredInitialGenerate)
|
||||||
hasTriggeredInitialGenerate = true
|
hasTriggeredInitialGenerate = true
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
previewItems,
|
|
||||||
() => {
|
|
||||||
renderedPreviewCount.value = Math.min(PREVIEW_BATCH_SIZE, previewItems.value.length)
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -232,101 +394,6 @@ onBeforeUnmount(() => {
|
|||||||
previewGenerationToken += 1
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
let nextIndex = 0
|
|
||||||
|
|
||||||
const worker = async () => {
|
|
||||||
while (true) {
|
|
||||||
if (isStaleGeneration(generationToken)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = nextIndex
|
|
||||||
nextIndex += 1
|
|
||||||
if (currentIndex >= fonts.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontInfo = fonts[currentIndex]
|
|
||||||
if (!fontInfo) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fontInfo.loaded) {
|
|
||||||
try {
|
|
||||||
await fontStore.loadFont(fontInfo)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load font ${fontInfo.name}:`, error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStaleGeneration(generationToken) || !fontInfo.font) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const svgResult = await generateSvgWithPreviewCache(fontInfo)
|
|
||||||
if (!svgResult) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStaleGeneration(generationToken)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
items[currentIndex] = {
|
|
||||||
fontInfo,
|
|
||||||
svgResult,
|
|
||||||
selected: selectedFontIdSet.has(fontInfo.id),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to generate SVG for ${fontInfo.name}:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(Array.from({ length: workerCount }, () => worker()))
|
|
||||||
|
|
||||||
if (isStaleGeneration(generationToken)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
previewItems.value = items.filter((item): item is PreviewItemType => item !== null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate previews:', error)
|
|
||||||
} finally {
|
|
||||||
if (!isStaleGeneration(generationToken)) {
|
|
||||||
isGenerating.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelectItem(item: PreviewItemType) {
|
function toggleSelectItem(item: PreviewItemType) {
|
||||||
item.selected = !item.selected
|
item.selected = !item.selected
|
||||||
uiStore.toggleExportItem(item)
|
uiStore.toggleExportItem(item)
|
||||||
@@ -396,7 +463,7 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasMorePreviewItems" class="text-xs text-[#86909c] text-center py-2">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user