419 lines
14 KiB
Vue
419 lines
14 KiB
Vue
<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>
|