update at 2026-02-07 11:14:09
This commit is contained in:
332
frontend/src/App.vue
Normal file
332
frontend/src/App.vue
Normal 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>
|
||||
Reference in New Issue
Block a user