update at 2026-02-07 13:57:01
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFontLoader } from './composables/useFontLoader'
|
||||
import { useUiStore } from './stores/uiStore'
|
||||
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 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))
|
||||
})
|
||||
|
||||
// 加载字体列表
|
||||
try {
|
||||
useFontLoader()
|
||||
@@ -164,6 +182,10 @@ function handleTextInput(event: Event) {
|
||||
uiStore.setInputText(wrappedText)
|
||||
}
|
||||
|
||||
function handleTogglePreviewSelectAll() {
|
||||
svgPreviewRef.value?.toggleSelectAllPreviewItems()
|
||||
}
|
||||
|
||||
console.log('App.vue: script setup completed')
|
||||
</script>
|
||||
|
||||
@@ -291,9 +313,22 @@ console.log('App.vue: script setup completed')
|
||||
|
||||
<!-- 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 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 />
|
||||
<SvgPreview ref="svgPreviewRef" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
6
frontend/src/assets/icons/selectall.svg
Normal file
6
frontend/src/assets/icons/selectall.svg
Normal 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 |
6
frontend/src/assets/icons/unselectall.svg
Normal file
6
frontend/src/assets/icons/unselectall.svg
Normal 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 |
@@ -1,4 +1,6 @@
|
||||
<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 { useFontStore } from '../stores/fontStore'
|
||||
|
||||
@@ -35,6 +37,36 @@ function isFavorite(node: FontTreeNode): boolean {
|
||||
function isInPreview(node: FontTreeNode): boolean {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -42,7 +74,7 @@ function isInPreview(node: FontTreeNode): boolean {
|
||||
<div v-for="node in nodes" :key="node.name">
|
||||
<!-- 分类节点 -->
|
||||
<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">
|
||||
<button
|
||||
@@ -71,6 +103,21 @@ function isInPreview(node: FontTreeNode): boolean {
|
||||
>
|
||||
{{ node.name }}
|
||||
</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>
|
||||
|
||||
<!-- 竖直连接线 -->
|
||||
|
||||
@@ -15,6 +15,9 @@ const previewFonts = computed(() => fontStore.previewFonts)
|
||||
const inputText = computed(() => uiStore.inputText)
|
||||
const fontSize = computed(() => uiStore.fontSize)
|
||||
const fillColor = computed(() => uiStore.textColor)
|
||||
const isAllPreviewSelected = computed(() => {
|
||||
return previewItems.value.length > 0 && previewItems.value.every(item => item.selected)
|
||||
})
|
||||
|
||||
watch(
|
||||
[previewFonts, inputText, fontSize, fillColor],
|
||||
@@ -81,6 +84,29 @@ function toggleSelectItem(item: PreviewItemType) {
|
||||
item.selected = !item.selected
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user