update at 2026-02-07 13:57:01

This commit is contained in:
douboer
2026-02-07 13:57:01 +08:00
parent 951eda9c58
commit d77f7446a2
5 changed files with 124 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref } from 'vue'
import { useFontLoader } from './composables/useFontLoader' import { useFontLoader } from './composables/useFontLoader'
import { useUiStore } from './stores/uiStore' import { useUiStore } from './stores/uiStore'
import { useFontStore } from './stores/fontStore' import { useFontStore } from './stores/fontStore'
@@ -7,17 +7,35 @@ import { MAX_CHARS_PER_LINE, wrapTextByChars } from './utils/text-layout'
import FontSelector from './components/FontSelector.vue' import FontSelector from './components/FontSelector.vue'
import FavoritesList from './components/FavoritesList.vue' import FavoritesList from './components/FavoritesList.vue'
import SvgPreview from './components/SvgPreview.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...') console.log('App.vue: script setup running...')
const uiStore = useUiStore() const uiStore = useUiStore()
const fontStore = useFontStore() const fontStore = useFontStore()
type SvgPreviewExpose = {
toggleSelectAllPreviewItems: () => void
}
const svgPreviewRef = ref<SvgPreviewExpose | null>(null)
const fontSizePercent = computed(() => { const fontSizePercent = computed(() => {
const raw = ((uiStore.fontSize - 10) / (500 - 10)) * 100 const raw = ((uiStore.fontSize - 10) / (500 - 10)) * 100
return Math.max(0, Math.min(100, raw)) 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))
})
// 加载字体列表 // 加载字体列表
try { try {
useFontLoader() useFontLoader()
@@ -164,6 +182,10 @@ function handleTextInput(event: Event) {
uiStore.setInputText(wrappedText) uiStore.setInputText(wrappedText)
} }
function handleTogglePreviewSelectAll() {
svgPreviewRef.value?.toggleSelectAllPreviewItems()
}
console.log('App.vue: script setup completed') console.log('App.vue: script setup completed')
</script> </script>
@@ -291,9 +313,22 @@ console.log('App.vue: script setup completed')
<!-- Frame 8: 右侧预览区 - 弹性宽度 --> <!-- 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-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 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"> <div v-overflow-aware class="scrollbar-hover flex-1 min-h-0 py-2 overflow-y-auto overflow-x-hidden">
<SvgPreview /> <SvgPreview ref="svgPreviewRef" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path fill="#8552A1" d="M10.114 9.583 7.966 7.61l-.716.658 2.864 2.633L16.25 5.26l-.716-.659-5.42 4.983Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M10.114 9.583 7.966 7.61l-.716.658 2.864 2.633L16.25 5.26l-.716-.659-5.42 4.983Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path fill="#8552A1" d="m15.523 4.546-.806-.705-3.194 2.795L8.328 3.84l-.805.705 3.194 2.795-3.194 2.795.805.705 3.195-2.795 3.194 2.795.806-.705-3.195-2.795 3.195-2.795Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path stroke="#8552A1" stroke-width=".5" d="m15.523 4.546-.806-.705-3.194 2.795L8.328 3.84l-.805.705 3.194 2.795-3.194 2.795.805.705 3.195-2.795 3.194 2.795.806-.705-3.195-2.795 3.195-2.795Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import selectAllIcon from '../assets/icons/selectall.svg'
import unselectAllIcon from '../assets/icons/unselectall.svg'
import type { FontTreeNode } from '../types/font' import type { FontTreeNode } from '../types/font'
import { useFontStore } from '../stores/fontStore' import { useFontStore } from '../stores/fontStore'
@@ -35,6 +37,36 @@ function isFavorite(node: FontTreeNode): boolean {
function isInPreview(node: FontTreeNode): boolean { function isInPreview(node: FontTreeNode): boolean {
return node.type === 'font' && node.fontInfo ? fontStore.previewFontIds.has(node.fontInfo.id) : false return node.type === 'font' && node.fontInfo ? fontStore.previewFontIds.has(node.fontInfo.id) : false
} }
function getCategoryFontIds(node: FontTreeNode): string[] {
if (node.type !== 'category' || !node.children) {
return []
}
return node.children
.filter((child): child is FontTreeNode & { fontInfo: NonNullable<FontTreeNode['fontInfo']> } => child.type === 'font' && !!child.fontInfo)
.map(child => child.fontInfo.id)
}
function isCategoryAllInPreview(node: FontTreeNode): boolean {
const ids = getCategoryFontIds(node)
return ids.length > 0 && ids.every(id => fontStore.previewFontIds.has(id))
}
function handleCategorySelectAll(node: FontTreeNode, event: Event) {
event.stopPropagation()
const ids = getCategoryFontIds(node)
if (ids.length === 0) {
return
}
if (isCategoryAllInPreview(node)) {
ids.forEach(id => fontStore.removeFromPreview(id))
} else {
ids.forEach(id => fontStore.addToPreview(id))
}
}
</script> </script>
<template> <template>
@@ -42,7 +74,7 @@ function isInPreview(node: FontTreeNode): boolean {
<div v-for="node in nodes" :key="node.name"> <div v-for="node in nodes" :key="node.name">
<!-- 分类节点 --> <!-- 分类节点 -->
<div v-if="node.type === 'category'" class="relative mb-3"> <div v-if="node.type === 'category'" class="relative mb-3">
<div class="flex items-center"> <div class="flex items-center gap-2">
<!-- 左侧展开图标 --> <!-- 左侧展开图标 -->
<div class="tree-icon-wrapper"> <div class="tree-icon-wrapper">
<button <button
@@ -71,6 +103,21 @@ function isInPreview(node: FontTreeNode): boolean {
> >
{{ node.name }} {{ node.name }}
</div> </div>
<div class="flex items-center gap-2 shrink-0 mr-[1px]">
<button
@click="handleCategorySelectAll(node, $event)"
class="w-4 h-4 shrink-0 p-0 border-0 bg-transparent cursor-pointer hover:opacity-85 transition-opacity"
title="分类全选/全不选"
>
<img
:src="isCategoryAllInPreview(node) ? unselectAllIcon : selectAllIcon"
alt="分类全选/全不选"
class="w-full h-full"
/>
</button>
<div class="w-[18px] h-[17px] shrink-0" aria-hidden="true"></div>
</div>
</div> </div>
<!-- 竖直连接线 --> <!-- 竖直连接线 -->

View File

@@ -15,6 +15,9 @@ const previewFonts = computed(() => fontStore.previewFonts)
const inputText = computed(() => uiStore.inputText) const inputText = computed(() => uiStore.inputText)
const fontSize = computed(() => uiStore.fontSize) const fontSize = computed(() => uiStore.fontSize)
const fillColor = computed(() => uiStore.textColor) const fillColor = computed(() => uiStore.textColor)
const isAllPreviewSelected = computed(() => {
return previewItems.value.length > 0 && previewItems.value.every(item => item.selected)
})
watch( watch(
[previewFonts, inputText, fontSize, fillColor], [previewFonts, inputText, fontSize, fillColor],
@@ -81,6 +84,29 @@ function toggleSelectItem(item: PreviewItemType) {
item.selected = !item.selected item.selected = !item.selected
uiStore.toggleExportItem(item) uiStore.toggleExportItem(item)
} }
function toggleSelectAllPreviewItems() {
if (previewItems.value.length === 0) {
return
}
if (isAllPreviewSelected.value) {
uiStore.clearExportSelection()
previewItems.value.forEach(item => {
item.selected = false
})
return
}
previewItems.value.forEach(item => {
item.selected = true
})
uiStore.selectAllExportItems(previewItems.value)
}
defineExpose({
toggleSelectAllPreviewItems,
})
</script> </script>
<template> <template>