update at 2026-02-07 14:54:45

This commit is contained in:
douboer
2026-02-07 14:54:45 +08:00
parent 7fcb84aed6
commit 6733ce3e9f
2 changed files with 121 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { useFontStore } from '../stores/fontStore' import { useFontStore } from '../stores/fontStore'
import { useUiStore } from '../stores/uiStore' import { useUiStore } from '../stores/uiStore'
import { generateSvg } from '../utils/svg-builder' import { generateSvg } from '../utils/svg-builder'
@@ -15,44 +15,112 @@ 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_CONCURRENCY = 4
let previewGenerateTimer: ReturnType<typeof setTimeout> | null = null
let previewGenerationToken = 0
let hasTriggeredInitialGenerate = false
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)
}) })
function isStaleGeneration(token: number): boolean {
return token !== previewGenerationToken
}
function scheduleGeneratePreviews(withDebounce = true) {
if (previewGenerateTimer !== null) {
clearTimeout(previewGenerateTimer)
previewGenerateTimer = null
}
if (!withDebounce) {
void generatePreviews()
return
}
previewGenerateTimer = setTimeout(() => {
previewGenerateTimer = null
void generatePreviews()
}, PREVIEW_DEBOUNCE_MS)
}
watch( watch(
[previewFonts, inputText, fontSize, fillColor], [previewFonts, inputText, fontSize, fillColor],
async () => { () => {
await generatePreviews() scheduleGeneratePreviews(hasTriggeredInitialGenerate)
hasTriggeredInitialGenerate = true
}, },
{ immediate: true } { immediate: true }
) )
onBeforeUnmount(() => {
if (previewGenerateTimer !== null) {
clearTimeout(previewGenerateTimer)
previewGenerateTimer = null
}
previewGenerationToken += 1
})
async function generatePreviews() { async function generatePreviews() {
const generationToken = ++previewGenerationToken
const validPreviewFontIds = new Set(previewFonts.value.map(font => font.id)) const validPreviewFontIds = new Set(previewFonts.value.map(font => font.id))
uiStore.retainExportItemsByFontIds(validPreviewFontIds) uiStore.retainExportItemsByFontIds(validPreviewFontIds)
if (!inputText.value || inputText.value.trim() === '') { if (!inputText.value || inputText.value.trim() === '') {
if (!isStaleGeneration(generationToken)) {
previewItems.value = [] previewItems.value = []
}
return return
} }
const fonts = previewFonts.value const fonts = previewFonts.value
if (fonts.length === 0) { if (fonts.length === 0) {
if (!isStaleGeneration(generationToken)) {
previewItems.value = [] previewItems.value = []
}
return return
} }
isGenerating.value = true isGenerating.value = true
try { try {
const items: PreviewItemType[] = [] 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
for (const fontInfo of fonts) { const worker = async () => {
if (!fontInfo.loaded) { while (true) {
await fontStore.loadFont(fontInfo) 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
} }
if (fontInfo.font) {
try { try {
const svgResult = await generateSvg({ const svgResult = await generateSvg({
text: inputText.value, text: inputText.value,
@@ -61,23 +129,35 @@ async function generatePreviews() {
fillColor: fillColor.value, fillColor: fillColor.value,
}) })
items.push({ if (isStaleGeneration(generationToken)) {
return
}
items[currentIndex] = {
fontInfo, fontInfo,
svgResult, svgResult,
selected: uiStore.selectedExportItems.some(item => item.fontInfo.id === fontInfo.id) selected: selectedFontIdSet.has(fontInfo.id),
}) }
} catch (error) { } catch (error) {
console.error(`Failed to generate SVG for ${fontInfo.name}:`, error) console.error(`Failed to generate SVG for ${fontInfo.name}:`, error)
} }
} }
} }
previewItems.value = items await Promise.all(Array.from({ length: workerCount }, () => worker()))
if (isStaleGeneration(generationToken)) {
return
}
previewItems.value = items.filter((item): item is PreviewItemType => item !== null)
} catch (error) { } catch (error) {
console.error('Failed to generate previews:', error) console.error('Failed to generate previews:', error)
} finally { } finally {
if (!isStaleGeneration(generationToken)) {
isGenerating.value = false isGenerating.value = false
} }
}
} }
function toggleSelectItem(item: PreviewItemType) { function toggleSelectItem(item: PreviewItemType) {

View File

@@ -31,6 +31,7 @@ export const useFontStore = defineStore('font', () => {
const expandedCategoryNames = ref<Set<string>>(readSet('font.expandedCategories')) const expandedCategoryNames = ref<Set<string>>(readSet('font.expandedCategories'))
const fontTree = ref<FontTreeNode[]>([]) const fontTree = ref<FontTreeNode[]>([])
const isLoadingFonts = ref(false) const isLoadingFonts = ref(false)
const loadingFontTasks = new Map<string, Promise<void>>()
// 计算属性 // 计算属性
const selectedFonts = computed(() => { const selectedFonts = computed(() => {
@@ -151,6 +152,13 @@ export const useFontStore = defineStore('font', () => {
return return
} }
const existingTask = loadingFontTasks.get(fontInfo.id)
if (existingTask) {
await existingTask
return
}
const loadingTask = (async () => {
try { try {
const font = await loadFontWithProgress(fontInfo.path, (progress) => { const font = await loadFontWithProgress(fontInfo.path, (progress) => {
fontInfo.progress = progress fontInfo.progress = progress
@@ -161,7 +169,14 @@ export const useFontStore = defineStore('font', () => {
} catch (error) { } catch (error) {
console.error(`Failed to load font ${fontInfo.name}:`, error) console.error(`Failed to load font ${fontInfo.name}:`, error)
throw error throw error
} finally {
loadingFontTasks.delete(fontInfo.id)
} }
})()
loadingFontTasks.set(fontInfo.id, loadingTask)
await loadingTask
} }
function buildFontTree(fontList: FontInfo[]): FontTreeNode[] { function buildFontTree(fontList: FontInfo[]): FontTreeNode[] {