update at 2026-02-11 19:31:11

This commit is contained in:
douboer
2026-02-11 19:31:11 +08:00
parent b0c7ea4cba
commit cc6c9c8a99
4 changed files with 47 additions and 10 deletions

View File

@@ -1 +1 @@
import{_ as v}from"./index-BF1ysyeL.js";function p(t,e,n="text/plain"){const o=new Blob([t],{type:n});s(o,e)}function s(t,e){const n=URL.createObjectURL(t),o=document.createElement("a");o.href=n,o.download=e,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(n)}function P(t,e){p(t,e,"image/svg+xml")}async function F(t,e="font2svg-export.zip"){const n=(await v(async()=>{const{default:r}=await import("./jszip.min-D7KnG0-e.js").then(i=>i.j);return{default:r}},[])).default,o=new n;for(const r of t)o.file(r.name,r.content);const a=await o.generateAsync({type:"blob"});s(a,e)}function d(t){if(!t)return null;const e=t.match(/-?\d+(\.\d+)?/);if(!e)return null;const n=Number(e[0]);return Number.isFinite(n)?n:null}function x(t){const n=new DOMParser().parseFromString(t,"image/svg+xml").documentElement,o=d(n.getAttribute("width")),a=d(n.getAttribute("height"));if(o&&a)return{width:o,height:a};const r=n.getAttribute("viewBox");if(r){const i=r.trim().split(/[\s,]+/).map(Number);if(i.length===4&&Number.isFinite(i[2])&&Number.isFinite(i[3]))return{width:Math.max(1,i[2]),height:Math.max(1,i[3])}}return{width:1024,height:1024}}async function _(t,e){const n=x(t),o=e?.scale??1,a=Math.max(1,Math.round((e?.width??n.width)*o)),r=Math.max(1,Math.round((e?.height??n.height)*o)),i=document.createElement("canvas");i.width=a,i.height=r;const l=i.getContext("2d");if(!l)throw new Error("无法创建 PNG 画布");e?.backgroundColor?(l.fillStyle=e.backgroundColor,l.fillRect(0,0,a,r)):l.clearRect(0,0,a,r);const f=new Blob([t],{type:"image/svg+xml;charset=utf-8"}),u=URL.createObjectURL(f);try{const c=new Image;await new Promise((w,b)=>{c.onload=()=>w(),c.onerror=()=>b(new Error("SVG 转 PNG 失败")),c.src=u}),l.drawImage(c,0,0,a,r)}finally{URL.revokeObjectURL(u)}const g=await new Promise(c=>{i.toBlob(c,"image/png")});if(!g)throw new Error("PNG 编码失败");return g}async function R(t,e,n){const o=await _(t,n);s(o,e)}function m(t){return t.replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").replace(/\s+/g,"_").substring(0,200)}function h(t,e){const n=m(Array.from(t).slice(0,8).join(""));return`${m(e.substring(0,20))}_${n}`}function B(t,e){return`${h(t,e)}.svg`}function L(t,e){return`${h(t,e)}.png`}export{_ as convertSvgToPngBlob,s as downloadBlob,F as downloadMultipleFiles,R as downloadPngFromSvg,P as downloadSvg,p as downloadText,L as generatePngFilename,B as generateSvgFilename,m as sanitizeFilename}; import{_ as v}from"./index-_4VXTXt_.js";function p(t,e,n="text/plain"){const o=new Blob([t],{type:n});s(o,e)}function s(t,e){const n=URL.createObjectURL(t),o=document.createElement("a");o.href=n,o.download=e,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(n)}function P(t,e){p(t,e,"image/svg+xml")}async function F(t,e="font2svg-export.zip"){const n=(await v(async()=>{const{default:r}=await import("./jszip.min-D7KnG0-e.js").then(i=>i.j);return{default:r}},[])).default,o=new n;for(const r of t)o.file(r.name,r.content);const a=await o.generateAsync({type:"blob"});s(a,e)}function d(t){if(!t)return null;const e=t.match(/-?\d+(\.\d+)?/);if(!e)return null;const n=Number(e[0]);return Number.isFinite(n)?n:null}function x(t){const n=new DOMParser().parseFromString(t,"image/svg+xml").documentElement,o=d(n.getAttribute("width")),a=d(n.getAttribute("height"));if(o&&a)return{width:o,height:a};const r=n.getAttribute("viewBox");if(r){const i=r.trim().split(/[\s,]+/).map(Number);if(i.length===4&&Number.isFinite(i[2])&&Number.isFinite(i[3]))return{width:Math.max(1,i[2]),height:Math.max(1,i[3])}}return{width:1024,height:1024}}async function _(t,e){const n=x(t),o=e?.scale??1,a=Math.max(1,Math.round((e?.width??n.width)*o)),r=Math.max(1,Math.round((e?.height??n.height)*o)),i=document.createElement("canvas");i.width=a,i.height=r;const l=i.getContext("2d");if(!l)throw new Error("无法创建 PNG 画布");e?.backgroundColor?(l.fillStyle=e.backgroundColor,l.fillRect(0,0,a,r)):l.clearRect(0,0,a,r);const f=new Blob([t],{type:"image/svg+xml;charset=utf-8"}),u=URL.createObjectURL(f);try{const c=new Image;await new Promise((w,b)=>{c.onload=()=>w(),c.onerror=()=>b(new Error("SVG 转 PNG 失败")),c.src=u}),l.drawImage(c,0,0,a,r)}finally{URL.revokeObjectURL(u)}const g=await new Promise(c=>{i.toBlob(c,"image/png")});if(!g)throw new Error("PNG 编码失败");return g}async function R(t,e,n){const o=await _(t,n);s(o,e)}function m(t){return t.replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").replace(/\s+/g,"_").substring(0,200)}function h(t,e){const n=m(Array.from(t).slice(0,8).join(""));return`${m(e.substring(0,20))}_${n}`}function B(t,e){return`${h(t,e)}.svg`}function L(t,e){return`${h(t,e)}.png`}export{_ as convertSvgToPngBlob,s as downloadBlob,F as downloadMultipleFiles,R as downloadPngFromSvg,P as downloadSvg,p as downloadText,L as generatePngFilename,B as generateSvgFilename,m as sanitizeFilename};

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
<link rel="apple-touch-icon" href="/favicon_new.png" /> <link rel="apple-touch-icon" href="/favicon_new.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Font2SVG - 字体转SVG工具</title> <title>Font2SVG - 字体转SVG工具</title>
<script type="module" crossorigin src="/assets/index-BF1ysyeL.js"></script> <script type="module" crossorigin src="/assets/index-_4VXTXt_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dl9L1Xmr.css"> <link rel="stylesheet" crossorigin href="/assets/index-Dl9L1Xmr.css">
</head> </head>
<body> <body>

View File

@@ -32,6 +32,7 @@ const activePreviewFonts = ref<FontInfo[]>([])
const processedFontCount = ref(0) const processedFontCount = ref(0)
const renderedPreviewCount = ref(0) const renderedPreviewCount = ref(0)
const previewTriggerItemEl = ref<HTMLElement | null>(null) const previewTriggerItemEl = ref<HTMLElement | null>(null)
const previewErrorMessage = ref('')
const previewFonts = computed(() => fontStore.previewFonts) const previewFonts = computed(() => fontStore.previewFonts)
const inputText = computed(() => uiStore.inputText) const inputText = computed(() => uiStore.inputText)
@@ -302,17 +303,18 @@ async function generatePreviewBatch(
startIndex: number, startIndex: number,
batchSize: number, batchSize: number,
generationToken: number, generationToken: number,
): Promise<PreviewRenderItem[]> { ): Promise<{ items: PreviewRenderItem[]; errors: string[] }> {
const endIndex = Math.min(startIndex + batchSize, fonts.length) const endIndex = Math.min(startIndex + batchSize, fonts.length)
const batchFonts = fonts.slice(startIndex, endIndex) const batchFonts = fonts.slice(startIndex, endIndex)
if (batchFonts.length === 0) { if (batchFonts.length === 0) {
return [] return { items: [], errors: [] }
} }
const selectedFontIdSet = new Set(uiStore.selectedExportItems.map(item => item.fontInfo.id)) const selectedFontIdSet = new Set(uiStore.selectedExportItems.map(item => item.fontInfo.id))
const items = new Array<PreviewRenderItem | null>(batchFonts.length).fill(null) const items = new Array<PreviewRenderItem | null>(batchFonts.length).fill(null)
const workerCount = Math.min(PREVIEW_CONCURRENCY, batchFonts.length) const workerCount = Math.min(PREVIEW_CONCURRENCY, batchFonts.length)
let nextIndex = 0 let nextIndex = 0
const errors: string[] = []
const worker = async () => { const worker = async () => {
while (true) { while (true) {
@@ -353,7 +355,9 @@ async function generatePreviewBatch(
} }
items[localIndex] = applyLocalStyleToPreviewItem(item) items[localIndex] = applyLocalStyleToPreviewItem(item)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`Failed to render preview for ${fontInfo.name}:`, error) console.error(`Failed to render preview for ${fontInfo.name}:`, error)
errors.push(`${fontInfo.name}: ${message}`)
} }
} }
} }
@@ -361,10 +365,13 @@ async function generatePreviewBatch(
await Promise.all(Array.from({ length: workerCount }, () => worker())) await Promise.all(Array.from({ length: workerCount }, () => worker()))
if (isStaleGeneration(generationToken)) { if (isStaleGeneration(generationToken)) {
return [] return { items: [], errors: [] }
} }
return items.filter((item): item is PreviewRenderItem => item !== null) return {
items: items.filter((item): item is PreviewRenderItem => item !== null),
errors,
}
} }
async function loadNextPreviewBatch(generationToken: number) { async function loadNextPreviewBatch(generationToken: number) {
@@ -396,13 +403,20 @@ async function loadNextPreviewBatch(generationToken: number) {
activePreviewFonts.value.length, activePreviewFonts.value.length,
) )
previewItems.value = [...previewItems.value, ...batchItems] previewItems.value = [...previewItems.value, ...batchItems.items]
renderedPreviewCount.value = Math.min( renderedPreviewCount.value = Math.min(
renderedPreviewCount.value + PREVIEW_BATCH_SIZE, renderedPreviewCount.value + PREVIEW_BATCH_SIZE,
previewItems.value.length, previewItems.value.length,
) )
if (batchItems.errors.length > 0 && previewItems.value.length === 0) {
previewErrorMessage.value = `预览生成失败:${batchItems.errors[0]}`
} else if (previewItems.value.length > 0) {
previewErrorMessage.value = ''
}
} catch (error) { } catch (error) {
console.error('Failed to load preview batch:', error) console.error('Failed to load preview batch:', error)
previewErrorMessage.value = `预览生成失败:${error instanceof Error ? error.message : String(error)}`
} finally { } finally {
if (!isStaleGeneration(generationToken)) { if (!isStaleGeneration(generationToken)) {
isBatchGenerating.value = false isBatchGenerating.value = false
@@ -440,6 +454,7 @@ async function regeneratePreviews() {
processedFontCount.value = 0 processedFontCount.value = 0
previewItems.value = [] previewItems.value = []
renderedPreviewCount.value = 0 renderedPreviewCount.value = 0
previewErrorMessage.value = ''
if (!inputText.value || inputText.value.trim() === '' || nextPreviewFonts.length === 0) { if (!inputText.value || inputText.value.trim() === '' || nextPreviewFonts.length === 0) {
isGenerating.value = false isGenerating.value = false
@@ -450,8 +465,18 @@ async function regeneratePreviews() {
try { try {
await loadNextPreviewBatch(generationToken) await loadNextPreviewBatch(generationToken)
if (
!isStaleGeneration(generationToken) &&
previewItems.value.length === 0 &&
nextPreviewFonts.length > 0 &&
inputText.value.trim() !== '' &&
!previewErrorMessage.value
) {
previewErrorMessage.value = '预览生成失败:服务未返回可用结果'
}
} catch (error) { } catch (error) {
console.error('Failed to regenerate previews:', error) console.error('Failed to regenerate previews:', error)
previewErrorMessage.value = `预览生成失败:${error instanceof Error ? error.message : String(error)}`
} finally { } finally {
if (!isStaleGeneration(generationToken)) { if (!isStaleGeneration(generationToken)) {
isGenerating.value = false isGenerating.value = false
@@ -526,7 +551,19 @@ defineExpose({
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div v-if="previewItems.length === 0" class="text-[#86909c] text-center py-20"> <div v-if="previewItems.length === 0" class="text-[#86909c] text-center py-20">
{{ isGenerating ? '生成预览中...' : '请选择字体并输入内容' }} {{
isGenerating
? '生成预览中...'
: (
previewErrorMessage
? previewErrorMessage
: (
inputText.trim() === '' || previewFonts.length === 0
? '请选择字体并输入内容'
: '暂无可显示预览'
)
)
}}
</div> </div>
<div v-else class="flex flex-col gap-2"> <div v-else class="flex flex-col gap-2">