update at 2026-02-07 15:41:30
This commit is contained in:
@@ -321,7 +321,6 @@ console.log('App.vue: script setup completed')
|
|||||||
<div class="flex flex-col gap-2 shrink-0 overflow-hidden" style="flex-basis: 400px; max-width: 480px; min-width: 320px;">
|
<div class="flex flex-col gap-2 shrink-0 overflow-hidden" style="flex-basis: 400px; max-width: 480px; min-width: 320px;">
|
||||||
<!-- Frame 5: 字体选择 - 弹性高度 -->
|
<!-- 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 class="flex-[2] border border-solid border-[#f7e0e0] rounded-lg p-1.5 flex flex-col gap-2 overflow-hidden min-h-0">
|
||||||
<h2 class="text-base text-black shrink-0 leading-none">字体选择</h2>
|
|
||||||
<div v-overflow-aware class="scrollbar-hover flex-1 overflow-y-auto overflow-x-hidden pr-2">
|
<div v-overflow-aware class="scrollbar-hover flex-1 overflow-y-auto overflow-x-hidden pr-2">
|
||||||
<FontSelector />
|
<FontSelector />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
4
frontend/src/assets/icons/search.svg
Normal file
4
frontend/src/assets/icons/search.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" fill="#8552A1" rx="10" transform="matrix(-1 0 0 1 32 0)"/>
|
||||||
|
<path fill="#FEFDFE" d="m24.845 22.204-5.101-5.105-2.64 2.64 5.105 5.104a1.085 1.085 0 0 0 1.527 0l1.108-1.108a1.09 1.09 0 0 0 0-1.531ZM17.22 18.6l1.382-1.382-1.576-1.576a5.512 5.512 0 0 0-.63-7.032 5.51 5.51 0 0 0-7.785 0c-2.15 2.146-2.146 5.635 0 7.785a5.512 5.512 0 0 0 7.033.63l1.575 1.576Zm-7.541-3.288a3.983 3.983 0 0 1 0-5.635 3.983 3.983 0 0 1 5.635 0 3.983 3.983 0 0 1 0 5.635 3.983 3.983 0 0 1-5.635 0Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 611 B |
@@ -1,18 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useFontStore } from '../stores/fontStore'
|
import { useFontStore } from '../stores/fontStore'
|
||||||
import FontTree from './FontTree.vue'
|
import FontTree from './FontTree.vue'
|
||||||
|
import searchIcon from '../assets/icons/search.svg'
|
||||||
|
|
||||||
const fontStore = useFontStore()
|
const fontStore = useFontStore()
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
const fontTree = computed(() => fontStore.fontTree)
|
const fontTree = computed(() => fontStore.fontTree)
|
||||||
|
const normalizedSearchKeyword = computed(() => searchKeyword.value.trim().toLowerCase())
|
||||||
|
const hasSearchKeyword = computed(() => normalizedSearchKeyword.value.length > 0)
|
||||||
|
const hasMatchedFonts = computed(() => {
|
||||||
|
if (!hasSearchKeyword.value) {
|
||||||
|
return fontTree.value.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = normalizedSearchKeyword.value
|
||||||
|
return fontTree.value.some((node) => {
|
||||||
|
if (node.type !== 'category') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name.toLowerCase().includes(keyword)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (node.children ?? []).some(child => {
|
||||||
|
return child.type === 'font' && child.name.toLowerCase().includes(keyword)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2 pb-1">
|
||||||
<div v-if="fontTree.length === 0" class="text-sm text-gray-500 text-center py-8">
|
<div class="sticky top-0 z-10 bg-white pt-1 pb-1">
|
||||||
暂无字体
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="text-[16px] leading-none text-black font-bold shrink-0">
|
||||||
|
选择预览字体
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="h-8 rounded-[10px] bg-[#F3EDF7] pl-2 flex items-center">
|
||||||
|
<input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
type="text"
|
||||||
|
placeholder="输入搜索字体名称"
|
||||||
|
aria-label="字体搜索"
|
||||||
|
class="flex-1 min-w-0 bg-transparent border-none outline-none text-[14px] text-black placeholder-[#a2a0a9]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-8 h-8 shrink-0 p-0 border-0 bg-transparent flex items-center justify-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img :src="searchIcon" alt="" class="w-[24px] h-[24px]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FontTree v-else :nodes="fontTree" />
|
|
||||||
|
<div v-if="!hasMatchedFonts" class="text-sm text-gray-500 text-center py-8">
|
||||||
|
{{ hasSearchKeyword ? '未找到匹配字体' : '暂无字体' }}
|
||||||
|
</div>
|
||||||
|
<FontTree
|
||||||
|
v-else
|
||||||
|
:nodes="fontTree"
|
||||||
|
:search-keyword="normalizedSearchKeyword"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import selectAllIcon from '../assets/icons/selectall.svg'
|
import selectAllIcon from '../assets/icons/selectall.svg'
|
||||||
import unselectAllIcon from '../assets/icons/unselectall.svg'
|
import unselectAllIcon from '../assets/icons/unselectall.svg'
|
||||||
import type { FontTreeNode } from '../types/font'
|
import type { FontTreeNode } from '../types/font'
|
||||||
@@ -6,11 +7,47 @@ import { useFontStore } from '../stores/fontStore'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nodes: FontTreeNode[]
|
nodes: FontTreeNode[]
|
||||||
|
searchKeyword?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const fontStore = useFontStore()
|
const fontStore = useFontStore()
|
||||||
|
const normalizedSearchKeyword = computed(() => (props.searchKeyword ?? '').trim().toLowerCase())
|
||||||
|
const isSearchMode = computed(() => normalizedSearchKeyword.value.length > 0)
|
||||||
|
|
||||||
|
type FontLeafNode = FontTreeNode & { fontInfo: NonNullable<FontTreeNode['fontInfo']> }
|
||||||
|
|
||||||
|
function getVisibleChildren(node: FontTreeNode): FontLeafNode[] {
|
||||||
|
if (node.type !== 'category' || !node.children) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontChildren = node.children.filter(
|
||||||
|
(child): child is FontLeafNode => child.type === 'font' && !!child.fontInfo,
|
||||||
|
)
|
||||||
|
if (!isSearchMode.value) {
|
||||||
|
return fontChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = normalizedSearchKeyword.value
|
||||||
|
if (node.name.toLowerCase().includes(keyword)) {
|
||||||
|
return fontChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontChildren.filter(child => child.name.toLowerCase().includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderCategory(node: FontTreeNode): boolean {
|
||||||
|
return node.type === 'category' && getVisibleChildren(node).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategoryExpanded(node: FontTreeNode): boolean {
|
||||||
|
return isSearchMode.value ? true : !!node.expanded
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpand(node: FontTreeNode) {
|
function toggleExpand(node: FontTreeNode) {
|
||||||
|
if (isSearchMode.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const next = !node.expanded
|
const next = !node.expanded
|
||||||
node.expanded = next
|
node.expanded = next
|
||||||
fontStore.setCategoryExpanded(node.name, next)
|
fontStore.setCategoryExpanded(node.name, next)
|
||||||
@@ -39,17 +76,11 @@ function isInPreview(node: FontTreeNode): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryFontIds(node: FontTreeNode): string[] {
|
function getCategoryFontIds(node: FontTreeNode): string[] {
|
||||||
if (node.type !== 'category' || !node.children) {
|
return getVisibleChildren(node).map(child => child.fontInfo.id)
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.children
|
|
||||||
.filter((child): child is FontTreeNode & { fontInfo: NonNullable<FontTreeNode['fontInfo']> } => child.type === 'font' && !!child.fontInfo)
|
|
||||||
.map(child => child.fontInfo.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryFontCount(node: FontTreeNode): number {
|
function getCategoryFontCount(node: FontTreeNode): number {
|
||||||
return getCategoryFontIds(node).length
|
return getVisibleChildren(node).length
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategoryAllInPreview(node: FontTreeNode): boolean {
|
function isCategoryAllInPreview(node: FontTreeNode): boolean {
|
||||||
@@ -77,19 +108,21 @@ function handleCategorySelectAll(node: FontTreeNode, event: Event) {
|
|||||||
<div class="space-y-0">
|
<div class="space-y-0">
|
||||||
<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="shouldRenderCategory(node)" class="relative mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- 左侧展开图标 -->
|
<!-- 左侧展开图标 -->
|
||||||
<div class="tree-icon-wrapper">
|
<div class="tree-icon-wrapper">
|
||||||
<button
|
<button
|
||||||
@click="toggleExpand(node)"
|
@click="toggleExpand(node)"
|
||||||
class="tree-toggle"
|
class="tree-toggle"
|
||||||
|
:disabled="isSearchMode"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="node.expanded"
|
v-if="isCategoryExpanded(node)"
|
||||||
src="../assets/icons/zhedie.svg"
|
src="../assets/icons/zhedie.svg"
|
||||||
alt="收起"
|
alt="收起"
|
||||||
class="w-[15px] h-[15px]"
|
class="w-[15px] h-[15px]"
|
||||||
|
:class="{ 'opacity-70': isSearchMode }"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
@@ -103,7 +136,8 @@ function handleCategorySelectAll(node: FontTreeNode, event: Event) {
|
|||||||
<!-- 分类标题 -->
|
<!-- 分类标题 -->
|
||||||
<div
|
<div
|
||||||
@click="toggleExpand(node)"
|
@click="toggleExpand(node)"
|
||||||
class="text-base font-medium text-black cursor-pointer flex-1 ml-2"
|
class="text-base font-medium text-black flex-1 ml-2"
|
||||||
|
:class="isSearchMode ? 'cursor-default' : 'cursor-pointer'"
|
||||||
>
|
>
|
||||||
{{ node.name }}({{ getCategoryFontCount(node) }}字体)
|
{{ node.name }}({{ getCategoryFontCount(node) }}字体)
|
||||||
</div>
|
</div>
|
||||||
@@ -125,12 +159,12 @@ function handleCategorySelectAll(node: FontTreeNode, event: Event) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 竖直连接线 -->
|
<!-- 竖直连接线 -->
|
||||||
<div v-if="node.expanded && node.children" class="tree-vertical-line"></div>
|
<div v-if="isCategoryExpanded(node) && getVisibleChildren(node).length > 0" class="tree-vertical-line"></div>
|
||||||
|
|
||||||
<!-- 字体列表 -->
|
<!-- 字体列表 -->
|
||||||
<div v-if="node.expanded && node.children" class="flex flex-col gap-3 mt-3">
|
<div v-if="isCategoryExpanded(node) && getVisibleChildren(node).length > 0" class="flex flex-col gap-3 mt-3">
|
||||||
<div
|
<div
|
||||||
v-for="child in node.children"
|
v-for="child in getVisibleChildren(node)"
|
||||||
:key="child.name"
|
:key="child.name"
|
||||||
class="flex items-center gap-2 border-b border-[#c9cdd4] pb-2 relative"
|
class="flex items-center gap-2 border-b border-[#c9cdd4] pb-2 relative"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user