update at 2026-02-07 14:16:46
This commit is contained in:
@@ -1,28 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import selectAllIcon from '../assets/icons/selectall.svg'
|
||||
import unselectAllIcon from '../assets/icons/unselectall.svg'
|
||||
import { deleteFontFile, renameFontFile } from '../services/font-file-api'
|
||||
import type { FontInfo, FontTreeNode } from '../types/font'
|
||||
import type { FontTreeNode } from '../types/font'
|
||||
import { useFontStore } from '../stores/fontStore'
|
||||
import { toFontInfoList } from '../utils/font-list'
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: FontTreeNode[]
|
||||
}>()
|
||||
|
||||
const fontStore = useFontStore()
|
||||
const contextMenu = ref<{
|
||||
visible: boolean
|
||||
x: number
|
||||
y: number
|
||||
fontInfo: FontInfo | null
|
||||
}>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fontInfo: null,
|
||||
})
|
||||
|
||||
function toggleExpand(node: FontTreeNode) {
|
||||
const next = !node.expanded
|
||||
@@ -81,105 +67,6 @@ function handleCategorySelectAll(node: FontTreeNode, event: Event) {
|
||||
ids.forEach(id => fontStore.addToPreview(id))
|
||||
}
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenu.value.visible = false
|
||||
contextMenu.value.fontInfo = null
|
||||
}
|
||||
|
||||
function openFontContextMenu(node: FontTreeNode, event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (node.type !== 'font' || !node.fontInfo) {
|
||||
closeContextMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const menuWidth = 140
|
||||
const menuHeight = 80
|
||||
const safeX = Math.min(event.clientX, window.innerWidth - menuWidth - 8)
|
||||
const safeY = Math.min(event.clientY, window.innerHeight - menuHeight - 8)
|
||||
|
||||
contextMenu.value = {
|
||||
visible: true,
|
||||
x: Math.max(8, safeX),
|
||||
y: Math.max(8, safeY),
|
||||
fontInfo: node.fontInfo,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRenameFromContextMenu() {
|
||||
const targetFont = contextMenu.value.fontInfo
|
||||
if (!targetFont) {
|
||||
return
|
||||
}
|
||||
|
||||
closeContextMenu()
|
||||
|
||||
const renamed = window.prompt('请输入新的字体名称', targetFont.name)
|
||||
if (renamed === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextName = renamed.trim()
|
||||
if (!nextName || nextName === targetFont.name) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const fontList = await renameFontFile(targetFont.path, nextName)
|
||||
fontStore.replaceFonts(toFontInfoList(fontList))
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '字体重命名失败'
|
||||
window.alert(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteFromContextMenu() {
|
||||
const targetFont = contextMenu.value.fontInfo
|
||||
if (!targetFont) {
|
||||
return
|
||||
}
|
||||
|
||||
closeContextMenu()
|
||||
|
||||
const confirmed = window.confirm(`确认删除字体“${targetFont.name}”?此操作不可恢复。`)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const fontList = await deleteFontFile(targetFont.path)
|
||||
fontStore.replaceFonts(toFontInfoList(fontList))
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '字体删除失败'
|
||||
window.alert(message)
|
||||
}
|
||||
}
|
||||
|
||||
function handleGlobalPointerDown() {
|
||||
if (!contextMenu.value.visible) {
|
||||
return
|
||||
}
|
||||
closeContextMenu()
|
||||
}
|
||||
|
||||
function handleGlobalKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
closeContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('pointerdown', handleGlobalPointerDown)
|
||||
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('pointerdown', handleGlobalPointerDown)
|
||||
window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -242,7 +129,6 @@ onBeforeUnmount(() => {
|
||||
v-for="child in node.children"
|
||||
:key="child.name"
|
||||
class="flex items-center gap-2 border-b border-[#c9cdd4] pb-2 relative"
|
||||
@contextmenu.prevent="openFontContextMenu(child, $event)"
|
||||
>
|
||||
<!-- 水平连接线 -->
|
||||
<div class="tree-horizontal-line"></div>
|
||||
@@ -283,29 +169,6 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="fixed z-[2000] min-w-[140px] rounded-md border border-[#d9d9d9] bg-white shadow-md overflow-hidden"
|
||||
:style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }"
|
||||
@pointerdown.stop
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<button
|
||||
class="w-full h-9 px-3 text-left text-sm text-[#1f2329] hover:bg-[#f2f3f5] border-0 bg-transparent cursor-pointer"
|
||||
@click="handleRenameFromContextMenu"
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
class="w-full h-9 px-3 text-left text-sm text-[#f53f3f] hover:bg-[#fff2f0] border-0 bg-transparent cursor-pointer"
|
||||
@click="handleDeleteFromContextMenu"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { onMounted } from 'vue'
|
||||
import { useFontStore } from '../stores/fontStore'
|
||||
import type { FontListItem } from '../types/font'
|
||||
import { toFontInfoList } from '../utils/font-list'
|
||||
import type { FontInfo } from '../types/font'
|
||||
|
||||
interface FontListItem {
|
||||
id: string
|
||||
name: string
|
||||
filename: string
|
||||
category: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export function useFontLoader() {
|
||||
const fontStore = useFontStore()
|
||||
@@ -22,8 +29,23 @@ export function useFontLoader() {
|
||||
const fontList: FontListItem[] = await response.json()
|
||||
console.log('Loaded font list:', fontList.length, 'fonts')
|
||||
|
||||
const nextFonts = toFontInfoList(fontList)
|
||||
fontStore.replaceFonts(nextFonts)
|
||||
// 转换为 FontInfo
|
||||
for (const item of fontList) {
|
||||
const fontInfo: FontInfo = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
category: item.category,
|
||||
isFavorite: false,
|
||||
loaded: false,
|
||||
progress: 0,
|
||||
}
|
||||
|
||||
fontStore.addFont(fontInfo)
|
||||
}
|
||||
|
||||
// 更新字体树
|
||||
fontStore.updateFontTree()
|
||||
|
||||
console.log(`Successfully loaded ${fontList.length} fonts`)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { FontListItem } from '../types/font'
|
||||
|
||||
interface FontMutationSuccessResponse {
|
||||
success: true
|
||||
fontList: FontListItem[]
|
||||
}
|
||||
|
||||
interface FontMutationErrorResponse {
|
||||
success: false
|
||||
message?: string
|
||||
}
|
||||
|
||||
type FontMutationResponse = FontMutationSuccessResponse | FontMutationErrorResponse
|
||||
|
||||
async function requestFontMutation(
|
||||
endpoint: string,
|
||||
payload: Record<string, string>,
|
||||
): Promise<FontListItem[]> {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
let body: FontMutationResponse | null = null
|
||||
try {
|
||||
body = (await response.json()) as FontMutationResponse
|
||||
} catch {
|
||||
// ignore parse errors and fallback to status text
|
||||
}
|
||||
|
||||
if (!response.ok || !body || body.success !== true) {
|
||||
const message =
|
||||
body && body.success === false && body.message
|
||||
? body.message
|
||||
: `请求失败(${response.status})`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return body.fontList
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名字体文件并返回最新字体列表。
|
||||
*/
|
||||
export function renameFontFile(fontPath: string, newName: string): Promise<FontListItem[]> {
|
||||
return requestFontMutation('/api/fonts/rename', {
|
||||
path: fontPath,
|
||||
newName,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字体文件并返回最新字体列表。
|
||||
*/
|
||||
export function deleteFontFile(fontPath: string): Promise<FontListItem[]> {
|
||||
return requestFontMutation('/api/fonts/delete', {
|
||||
path: fontPath,
|
||||
})
|
||||
}
|
||||
@@ -23,10 +23,6 @@ export const useFontStore = defineStore('font', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function filterSetByValidIds(source: Set<string>, validIds: Set<string>): Set<string> {
|
||||
return new Set(Array.from(source).filter(id => validIds.has(id)))
|
||||
}
|
||||
|
||||
// 状态
|
||||
const fonts = ref<FontInfo[]>([])
|
||||
const selectedFontIds = ref<Set<string>>(new Set())
|
||||
@@ -58,23 +54,6 @@ export const useFontStore = defineStore('font', () => {
|
||||
fonts.value.push(fontInfo)
|
||||
}
|
||||
|
||||
function replaceFonts(nextFonts: FontInfo[]) {
|
||||
const validIds = new Set(nextFonts.map(font => font.id))
|
||||
|
||||
selectedFontIds.value = filterSetByValidIds(selectedFontIds.value, validIds)
|
||||
favoriteFontIds.value = filterSetByValidIds(favoriteFontIds.value, validIds)
|
||||
previewFontIds.value = filterSetByValidIds(previewFontIds.value, validIds)
|
||||
|
||||
writeSet('font.favoriteFontIds', favoriteFontIds.value)
|
||||
writeSet('font.previewFontIds', previewFontIds.value)
|
||||
|
||||
fonts.value = nextFonts.map(font => ({
|
||||
...font,
|
||||
isFavorite: favoriteFontIds.value.has(font.id),
|
||||
}))
|
||||
updateFontTree()
|
||||
}
|
||||
|
||||
function removeFont(fontId: string) {
|
||||
const index = fonts.value.findIndex(f => f.id === fontId)
|
||||
if (index !== -1) {
|
||||
@@ -250,7 +229,6 @@ export const useFontStore = defineStore('font', () => {
|
||||
|
||||
// 方法
|
||||
addFont,
|
||||
replaceFonts,
|
||||
removeFont,
|
||||
selectFont,
|
||||
unselectFont,
|
||||
|
||||
16
frontend/src/types/font.d.ts
vendored
16
frontend/src/types/font.d.ts
vendored
@@ -22,22 +22,6 @@ export interface FontInfo {
|
||||
progress: number
|
||||
}
|
||||
|
||||
/**
|
||||
* fonts.json 字体清单项
|
||||
*/
|
||||
export interface FontListItem {
|
||||
/** 字体唯一标识 */
|
||||
id: string
|
||||
/** 字体文件名(不含扩展名) */
|
||||
name: string
|
||||
/** 字体文件名(含扩展名) */
|
||||
filename: string
|
||||
/** 字体分类(目录名) */
|
||||
category: string
|
||||
/** 静态访问路径 */
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 字体树节点
|
||||
*/
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { FontInfo, FontListItem } from '../types/font'
|
||||
|
||||
/**
|
||||
* 将 fonts.json 项转换为 FontInfo 运行时结构。
|
||||
*/
|
||||
export function toFontInfo(item: FontListItem): FontInfo {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
category: item.category,
|
||||
isFavorite: false,
|
||||
loaded: false,
|
||||
progress: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换字体清单。
|
||||
*/
|
||||
export function toFontInfoList(items: FontListItem[]): FontInfo[] {
|
||||
return items.map(toFontInfo)
|
||||
}
|
||||
Reference in New Issue
Block a user