update at 2026-02-07 14:16:46
This commit is contained in:
@@ -1,28 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref } 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 { deleteFontFile, renameFontFile } from '../services/font-file-api'
|
import type { FontTreeNode } from '../types/font'
|
||||||
import type { FontInfo, FontTreeNode } from '../types/font'
|
|
||||||
import { useFontStore } from '../stores/fontStore'
|
import { useFontStore } from '../stores/fontStore'
|
||||||
import { toFontInfoList } from '../utils/font-list'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nodes: FontTreeNode[]
|
nodes: FontTreeNode[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const fontStore = useFontStore()
|
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) {
|
function toggleExpand(node: FontTreeNode) {
|
||||||
const next = !node.expanded
|
const next = !node.expanded
|
||||||
@@ -81,105 +67,6 @@ function handleCategorySelectAll(node: FontTreeNode, event: Event) {
|
|||||||
ids.forEach(id => fontStore.addToPreview(id))
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -242,7 +129,6 @@ onBeforeUnmount(() => {
|
|||||||
v-for="child in node.children"
|
v-for="child in node.children"
|
||||||
: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"
|
||||||
@contextmenu.prevent="openFontContextMenu(child, $event)"
|
|
||||||
>
|
>
|
||||||
<!-- 水平连接线 -->
|
<!-- 水平连接线 -->
|
||||||
<div class="tree-horizontal-line"></div>
|
<div class="tree-horizontal-line"></div>
|
||||||
@@ -283,29 +169,6 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useFontStore } from '../stores/fontStore'
|
import { useFontStore } from '../stores/fontStore'
|
||||||
import type { FontListItem } from '../types/font'
|
import type { FontInfo } from '../types/font'
|
||||||
import { toFontInfoList } from '../utils/font-list'
|
|
||||||
|
interface FontListItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
filename: string
|
||||||
|
category: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useFontLoader() {
|
export function useFontLoader() {
|
||||||
const fontStore = useFontStore()
|
const fontStore = useFontStore()
|
||||||
@@ -22,8 +29,23 @@ export function useFontLoader() {
|
|||||||
const fontList: FontListItem[] = await response.json()
|
const fontList: FontListItem[] = await response.json()
|
||||||
console.log('Loaded font list:', fontList.length, 'fonts')
|
console.log('Loaded font list:', fontList.length, 'fonts')
|
||||||
|
|
||||||
const nextFonts = toFontInfoList(fontList)
|
// 转换为 FontInfo
|
||||||
fontStore.replaceFonts(nextFonts)
|
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`)
|
console.log(`Successfully loaded ${fontList.length} fonts`)
|
||||||
} catch (error) {
|
} 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 fonts = ref<FontInfo[]>([])
|
||||||
const selectedFontIds = ref<Set<string>>(new Set())
|
const selectedFontIds = ref<Set<string>>(new Set())
|
||||||
@@ -58,23 +54,6 @@ export const useFontStore = defineStore('font', () => {
|
|||||||
fonts.value.push(fontInfo)
|
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) {
|
function removeFont(fontId: string) {
|
||||||
const index = fonts.value.findIndex(f => f.id === fontId)
|
const index = fonts.value.findIndex(f => f.id === fontId)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -250,7 +229,6 @@ export const useFontStore = defineStore('font', () => {
|
|||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
addFont,
|
addFont,
|
||||||
replaceFonts,
|
|
||||||
removeFont,
|
removeFont,
|
||||||
selectFont,
|
selectFont,
|
||||||
unselectFont,
|
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
|
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