update at 2026-02-07 13:57:01
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
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">
|
<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>
|
||||||
|
|
||||||
<!-- 竖直连接线 -->
|
<!-- 竖直连接线 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user