220 lines
6.3 KiB
Vue
220 lines
6.3 KiB
Vue
<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'
|
|
|
|
const props = defineProps<{
|
|
nodes: FontTreeNode[]
|
|
}>()
|
|
|
|
const fontStore = useFontStore()
|
|
|
|
function toggleExpand(node: FontTreeNode) {
|
|
const next = !node.expanded
|
|
node.expanded = next
|
|
fontStore.setCategoryExpanded(node.name, next)
|
|
}
|
|
|
|
function handlePreviewClick(node: FontTreeNode, event: Event) {
|
|
event.stopPropagation()
|
|
if (node.type === 'font' && node.fontInfo) {
|
|
fontStore.togglePreview(node.fontInfo.id)
|
|
}
|
|
}
|
|
|
|
function handleFavoriteClick(node: FontTreeNode, event: Event) {
|
|
event.stopPropagation()
|
|
if (node.type === 'font' && node.fontInfo) {
|
|
fontStore.toggleFavorite(node.fontInfo.id)
|
|
}
|
|
}
|
|
|
|
function isFavorite(node: FontTreeNode): boolean {
|
|
return node.type === 'font' && node.fontInfo ? fontStore.favoriteFontIds.has(node.fontInfo.id) : false
|
|
}
|
|
|
|
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>
|
|
<div class="space-y-0">
|
|
<div v-for="node in nodes" :key="node.name">
|
|
<!-- 分类节点 -->
|
|
<div v-if="node.type === 'category'" class="relative mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<!-- 左侧展开图标 -->
|
|
<div class="tree-icon-wrapper">
|
|
<button
|
|
@click="toggleExpand(node)"
|
|
class="tree-toggle"
|
|
>
|
|
<img
|
|
v-if="node.expanded"
|
|
src="../assets/icons/zhedie.svg"
|
|
alt="收起"
|
|
class="w-[15px] h-[15px]"
|
|
/>
|
|
<img
|
|
v-else
|
|
src="../assets/icons/icons_idx%20_12.svg"
|
|
alt="展开"
|
|
class="w-[15px] h-[15px]"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 分类标题 -->
|
|
<div
|
|
@click="toggleExpand(node)"
|
|
class="text-base font-medium text-black cursor-pointer flex-1 ml-2"
|
|
>
|
|
{{ 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>
|
|
|
|
<!-- 竖直连接线 -->
|
|
<div v-if="node.expanded && node.children" class="tree-vertical-line"></div>
|
|
|
|
<!-- 字体列表 -->
|
|
<div v-if="node.expanded && node.children" class="flex flex-col gap-3 mt-3">
|
|
<div
|
|
v-for="child in node.children"
|
|
:key="child.name"
|
|
class="flex items-center gap-2 border-b border-[#c9cdd4] pb-2 relative"
|
|
>
|
|
<!-- 水平连接线 -->
|
|
<div class="tree-horizontal-line"></div>
|
|
|
|
<!-- 字体图标 -->
|
|
<div class="w-4 h-4 shrink-0 ml-[17px]">
|
|
<img src="../assets/icons/icons_idx%20_18.svg" alt="font" class="w-full h-full" />
|
|
</div>
|
|
|
|
<!-- 字体名称 -->
|
|
<div class="flex-1 text-xs text-[#86909c]">
|
|
{{ child.name }}
|
|
</div>
|
|
|
|
<!-- 预览复选框 -->
|
|
<button
|
|
@click="handlePreviewClick(child, $event)"
|
|
class="w-[18px] h-[18px] shrink-0 border rounded-full flex items-center justify-center p-0 bg-transparent"
|
|
:class="isInPreview(child) ? 'bg-[#9b6bc2] border-[#9b6bc2]' : 'border-[#c9cdd4]'"
|
|
>
|
|
<img v-if="isInPreview(child)" src="../assets/icons/checkbox.svg" alt="选中" class="w-[11px] h-[9px]" />
|
|
</button>
|
|
|
|
<!-- 收藏按钮 -->
|
|
<button
|
|
@click="handleFavoriteClick(child, $event)"
|
|
class="w-[18px] h-[17px] shrink-0 p-0 border-0 bg-transparent"
|
|
>
|
|
<img
|
|
src="../assets/icons/icons_idx%20_19.svg"
|
|
alt="收藏"
|
|
class="w-full h-full"
|
|
:class="isFavorite(child) ? 'favorite-active' : ''"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.tree-icon-wrapper {
|
|
position: relative;
|
|
width: 17px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tree-toggle {
|
|
width: 15px;
|
|
height: 15px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.tree-vertical-line {
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 20px;
|
|
bottom: 12px;
|
|
width: 1px;
|
|
background: #c9cdd4;
|
|
}
|
|
|
|
.tree-horizontal-line {
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 12px;
|
|
width: 10px;
|
|
height: 1px;
|
|
background: #c9cdd4;
|
|
}
|
|
|
|
.favorite-active {
|
|
filter: brightness(0) saturate(100%) invert(16%) sepia(96%) saturate(7491%) hue-rotate(356deg) brightness(99%) contrast(119%);
|
|
}
|
|
</style>
|