update at 2026-02-07 11:14:09

This commit is contained in:
douboer
2026-02-07 11:14:09 +08:00
parent 2d18aa5137
commit 591bd9ba05
67 changed files with 4885 additions and 0 deletions

332
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,332 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useFontLoader } from './composables/useFontLoader'
import { useUiStore } from './stores/uiStore'
import { useFontStore } from './stores/fontStore'
import { MAX_CHARS_PER_LINE, wrapTextByChars } from './utils/text-layout'
import FontSelector from './components/FontSelector.vue'
import FavoritesList from './components/FavoritesList.vue'
import SvgPreview from './components/SvgPreview.vue'
console.log('App.vue: script setup running...')
const uiStore = useUiStore()
const fontStore = useFontStore()
const fontSizePercent = computed(() => {
const raw = ((uiStore.fontSize - 10) / (500 - 10)) * 100
return Math.max(0, Math.min(100, raw))
})
// 加载字体列表
try {
useFontLoader()
console.log('App.vue: useFontLoader called successfully')
} catch (error) {
console.error('App.vue: Error in useFontLoader:', error)
}
const normalizedInitialInput = wrapTextByChars(uiStore.inputText, MAX_CHARS_PER_LINE)
if (normalizedInitialInput !== uiStore.inputText) {
uiStore.setInputText(normalizedInitialInput)
}
type ExportFormat = 'svg' | 'png'
async function handleExport(format: ExportFormat) {
uiStore.retainExportItemsByFontIds(fontStore.previewFontIds)
const selectedItems = uiStore.selectedExportItems
const inputText = uiStore.inputText.trim()
if (selectedItems.length === 0) {
alert('请选择需要导出的效果')
return
}
if (!inputText) {
alert('请输入要导出的文字')
return
}
try {
const { generateSvg } = await import('./utils/svg-builder')
const {
convertSvgToPngBlob,
downloadSvg,
downloadMultipleFiles,
downloadPngFromSvg,
generatePngFilename,
generateSvgFilename,
} = await import('./utils/download')
if (selectedItems.length === 1) {
// 单个字体,直接下载 SVG
const item = selectedItems[0]
const svgResult = await generateSvg({
text: inputText,
font: item.fontInfo.font,
fontSize: uiStore.fontSize,
fillColor: uiStore.textColor,
letterSpacing: 0
})
if (format === 'svg') {
const filename = generateSvgFilename(inputText, svgResult.fontName)
downloadSvg(svgResult.svg, filename)
} else {
const filename = generatePngFilename(inputText, svgResult.fontName)
await downloadPngFromSvg(svgResult.svg, filename, {
width: svgResult.width,
height: svgResult.height,
})
}
} else {
// 多个字体,打包下载
const files: Array<{ name: string; content: string | Blob }> = []
for (const item of selectedItems) {
try {
const svgResult = await generateSvg({
text: inputText,
font: item.fontInfo.font,
fontSize: uiStore.fontSize,
fillColor: uiStore.textColor,
letterSpacing: 0
})
if (format === 'svg') {
const filename = generateSvgFilename(inputText, svgResult.fontName)
files.push({
name: filename,
content: svgResult.svg
})
} else {
const filename = generatePngFilename(inputText, svgResult.fontName)
const pngBlob = await convertSvgToPngBlob(svgResult.svg, {
width: svgResult.width,
height: svgResult.height,
})
files.push({
name: filename,
content: pngBlob,
})
}
} catch (error) {
console.warn(`字体 ${item.fontInfo.name} 导出失败:`, error)
}
}
if (files.length > 0) {
const zipFilename = format === 'svg' ? 'font2svg-svg-export.zip' : 'font2svg-png-export.zip'
await downloadMultipleFiles(files, zipFilename)
} else {
alert(`所有字体${format.toUpperCase()}导出都失败了`)
}
}
} catch (error) {
console.error('导出失败:', error)
alert(`导出失败: ${error instanceof Error ? error.message : '未知错误'}`)
}
}
function handleFontSizeChange(size: number) {
uiStore.setFontSize(size)
}
function handleSliderInput(event: Event) {
const target = event.target as HTMLInputElement
const value = Number(target.value)
if (!Number.isNaN(value)) {
uiStore.setFontSize(value)
}
}
function handleTextInput(event: Event) {
const target = event.target as HTMLTextAreaElement
const wrappedText = wrapTextByChars(target.value, MAX_CHARS_PER_LINE)
if (wrappedText !== target.value) {
target.value = wrappedText
}
uiStore.setInputText(wrappedText)
}
console.log('App.vue: script setup completed')
</script>
<template>
<div class="w-screen h-screen box-border p-8px bg-white flex flex-col overflow-hidden">
<!-- Frame 7: 顶部工具栏 -->
<div class="flex gap-2 items-center shrink-0 h-24 px-2 py-1">
<!-- webicon - 48x48 -->
<div class="w-12 h-12 rounded-xl overflow-hidden shrink-0">
<img src="/assets/webicon.svg" alt="logo" class="w-full h-full object-cover" />
</div>
<!-- 星程字体转换 - 弹性宽度 -->
<div class="shrink-0 max-w-[225px] min-w-[120px]" style="height: 72px;">
<img src="/assets/icons/星程字体转换.svg" alt="星程SVG文字生成 TEXT to SVG" class="w-full h-full object-contain" />
</div>
<!-- slider - 增加宽度 -->
<div class="flex items-center gap-3 px-2 shrink-0 relative" style="width: 280px; height: 32px;">
<button
@click="handleFontSizeChange(uiStore.fontSize - 10)"
class="w-4 h-4 shrink-0 cursor-pointer hover:opacity-70 transition-opacity flex items-center justify-center p-0 border-0 bg-transparent"
title="减小字体"
>
<img src="/assets/icons/icons_idx%20_38.svg" alt="A-" class="w-4 h-4 object-contain" />
</button>
<div class="flex-1 h-6 flex items-center relative">
<input
type="range"
min="10"
max="500"
step="1"
:value="uiStore.fontSize"
@input="handleSliderInput"
class="font-size-range w-full h-6 cursor-pointer"
:style="{ background: `linear-gradient(to right, #9b6bc2 0%, #9b6bc2 ${fontSizePercent}%, #e5e6eb ${fontSizePercent}%, #e5e6eb 100%)` }"
/>
<!-- 字体大小数字显示 -->
<div
class="absolute pointer-events-none -top-4"
:style="{ left: `calc(${fontSizePercent}% - 7px)` }"
>
<div class="text-[#8552A1] text-[12px] font-medium text-center w-6">
{{ uiStore.fontSize }}
</div>
</div>
</div>
<button
@click="handleFontSizeChange(uiStore.fontSize + 10)"
class="w-6 h-6 shrink-0 cursor-pointer hover:opacity-70 transition-opacity flex items-center justify-center p-0 border-0 bg-transparent"
title="增大字体"
>
<img src="/assets/icons/icons_idx%20_33.svg" alt="A+" class="w-6 h-6 object-contain" />
</button>
</div>
<!-- 文字颜色选择 -->
<div class="shrink-0 relative w-9 h-9">
<label class="w-full h-full flex items-center justify-center cursor-pointer">
<img src="/assets/icons/choose-color.svg" alt="颜色" class="w-9 h-9 object-contain" />
<input
type="color"
:value="uiStore.textColor"
@input="uiStore.setTextColor(($event.target as HTMLInputElement).value)"
class="absolute inset-0 opacity-0 cursor-pointer"
aria-label="选择文字颜色"
/>
</label>
</div>
<!-- Frame 14: 输入框 - 弹性宽度 -->
<div class="flex-1 min-w-[80px] bg-[#f7f8fa] rounded-lg px-2 py-1 h-12">
<textarea
:value="uiStore.inputText"
@input="handleTextInput"
placeholder="此处输入内容"
class="w-full h-full bg-transparent border-none outline-none text-base text-[#4e5969] placeholder-[#4e5969] resize-none leading-5 overflow-y-auto"
/>
</div>
<!-- Export Group -->
<div class="flex items-center gap-1 shrink-0 border border-[#8552A1] rounded-lg px-1 py-1 bg-[#f7f8fa] shadow-sm">
<div class="w-[18px] h-[42px] shrink-0 pointer-events-none">
<img src="/assets/icons/export.svg" alt="导出" class="w-full h-full object-contain" />
</div>
<button
@click="handleExport('svg')"
class="w-12 h-12 shrink-0 cursor-pointer hover:opacity-85 transition-opacity flex items-center justify-center p-0 border-0 bg-transparent"
title="导出 SVG"
>
<img src="/assets/icons/export-svg.svg" alt="导出SVG" class="w-12 h-12 object-contain" />
</button>
<button
@click="handleExport('png')"
class="w-12 h-12 shrink-0 cursor-pointer hover:opacity-85 transition-opacity flex items-center justify-center p-0 border-0 bg-transparent"
title="导出 PNG"
>
<img src="/assets/icons/export-png.svg" alt="导出PNG" class="w-12 h-12 object-contain" />
</button>
</div>
</div>
<!-- Frame 9: 主内容区 -->
<div class="flex-1 flex gap-2 min-h-0 overflow-hidden px-2">
<!-- Frame 15: 左侧栏 - 弹性宽度 -->
<div class="flex flex-col gap-2 shrink-0 overflow-hidden" style="flex-basis: 400px; max-width: 480px; min-width: 320px;">
<!-- Frame 5: 字体选择 - 弹性高度 -->
<div class="flex-[2] border border-solid border-[#f7e0e0] rounded-lg p-1.5 flex flex-col gap-2 overflow-hidden min-h-0">
<h2 class="text-base text-black shrink-0 leading-none">字体选择</h2>
<div v-overflow-aware class="scrollbar-hover flex-1 overflow-y-auto overflow-x-hidden pr-2">
<FontSelector />
</div>
</div>
<!-- Frame 5: 已收藏字体 - 弹性高度 -->
<div class="border border-solid border-[#f7e0e0] rounded-lg p-1.5 flex flex-col gap-2 flex-1 overflow-hidden min-h-[120px]">
<h2 class="text-base text-black shrink-0 leading-none">已收藏字体</h2>
<div v-overflow-aware class="scrollbar-hover flex-1 overflow-y-auto overflow-x-hidden pr-2">
<FavoritesList />
</div>
</div>
</div>
<!-- Frame 8: 右侧预览区 - 弹性宽度 -->
<div class="flex-1 border border-solid border-[#f7e0e0] rounded-lg p-1.5 flex flex-col gap-2 overflow-hidden min-w-0">
<h2 class="text-base text-black shrink-0 leading-none">效果预览</h2>
<div v-overflow-aware class="scrollbar-hover flex-1 min-h-0 py-2 overflow-y-auto overflow-x-hidden">
<SvgPreview />
</div>
</div>
</div>
<!-- 底部版权 -->
<div class="text-[#86909c] text-xs text-center shrink-0 h-6 pt-4 flex items-center justify-center px-2">
@版权说明所有字体来源互联网分享仅供效果预览不做下载传播如有侵权请告知douboer@gmail.com
</div>
</div>
</template>
<style scoped>
.font-size-range {
-webkit-appearance: none;
appearance: none;
height: 3px;
border-radius: 10px;
background: #e5e6eb;
}
.font-size-range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e6eb;
cursor: pointer;
}
.font-size-range::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e6eb;
cursor: pointer;
}
.font-size-range::-moz-range-track {
height: 3px;
border-radius: 10px;
background: transparent;
}
</style>