Files
font2pic/frontend/src/App.vue
2026-02-07 15:41:30 +08:00

419 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, ref } 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'
import selectAllIcon from './assets/icons/selectall.svg'
import unselectAllIcon from './assets/icons/unselectall.svg'
console.log('App.vue: script setup running...')
const uiStore = useUiStore()
const fontStore = useFontStore()
type SvgPreviewExpose = {
toggleSelectAllPreviewItems: () => void
}
const svgPreviewRef = ref<SvgPreviewExpose | null>(null)
const fontSizePercent = computed(() => {
const raw = ((uiStore.fontSize - 10) / (500 - 10)) * 100
return Math.max(0, Math.min(100, raw))
})
const isAllPreviewSelected = computed(() => {
const previewIds = fontStore.previewFonts.map(font => font.id)
if (previewIds.length === 0) {
return false
}
const selectedIds = new Set(uiStore.selectedExportItems.map(item => item.fontInfo.id))
return previewIds.every(id => selectedIds.has(id))
})
const isAllFavoriteSelected = computed(() => {
const favoriteIds = fontStore.favoriteFonts.map(font => font.id)
if (favoriteIds.length === 0) {
return false
}
return favoriteIds.every(id => fontStore.previewFontIds.has(id))
})
const favoriteFontCount = computed(() => fontStore.favoriteFonts.length)
// 加载字体列表
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]
if (!item?.fontInfo.font) {
alert('选中字体未加载完成,请稍后重试')
return
}
const font = item.fontInfo.font
const svgResult = await generateSvg({
text: inputText,
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 font = item.fontInfo.font
if (!font) {
console.warn(`字体 ${item.fontInfo.name} 尚未加载,已跳过导出`)
continue
}
const svgResult = await generateSvg({
text: inputText,
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)
}
function handleTogglePreviewSelectAll() {
svgPreviewRef.value?.toggleSelectAllPreviewItems()
}
function handleToggleFavoriteSelectAll() {
const favoriteIds = fontStore.favoriteFonts.map(font => font.id)
if (favoriteIds.length === 0) {
return
}
if (isAllFavoriteSelected.value) {
favoriteIds.forEach(id => fontStore.removeFromPreview(id))
return
}
favoriteIds.forEach(id => fontStore.addToPreview(id))
}
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">
<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]">
<div class="flex items-center pr-[9px]">
<h2 class="text-base text-black shrink-0 leading-none flex-1">
已收藏字体{{ favoriteFontCount }}字体
</h2>
<button
@click="handleToggleFavoriteSelectAll"
class="w-4 h-4 shrink-0 p-0 border-0 bg-transparent cursor-pointer hover:opacity-85 transition-opacity"
title="已收藏字体全选/全不选"
>
<img
:src="isAllFavoriteSelected ? unselectAllIcon : selectAllIcon"
alt="已收藏字体全选/全不选"
class="w-full h-full"
/>
</button>
</div>
<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">
<div class="flex items-center pr-[9px]">
<h2 class="text-base text-black shrink-0 leading-none flex-1">效果预览</h2>
<button
@click="handleTogglePreviewSelectAll"
class="w-4 h-4 shrink-0 p-0 border-0 bg-transparent cursor-pointer hover:opacity-85 transition-opacity"
title="效果预览全选/全不选"
>
<img
:src="isAllPreviewSelected ? unselectAllIcon : selectAllIcon"
alt="效果预览全选/全不选"
class="w-full h-full"
/>
</button>
</div>
<div v-overflow-aware class="scrollbar-hover flex-1 min-h-0 py-2 overflow-y-auto overflow-x-hidden">
<SvgPreview ref="svgPreviewRef" />
</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>