update at 2026-02-07 11:14:09

This commit is contained in:
douboer
2026-02-07 11:14:09 +08:00
parent 2d18aa5137
commit 591bd9ba05
67 changed files with 4885 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { FontInfo, FontTreeNode } from '../types/font'
import { loadFontWithProgress } from '../utils/font-loader'
export const useFontStore = defineStore('font', () => {
function readSet(key: string): Set<string> {
try {
const raw = localStorage.getItem(key)
if (!raw) return new Set()
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? new Set(parsed.map(String)) : new Set()
} catch {
return new Set()
}
}
function writeSet(key: string, value: Set<string>) {
try {
localStorage.setItem(key, JSON.stringify(Array.from(value)))
} catch {
// ignore storage errors
}
}
// 状态
const fonts = ref<FontInfo[]>([])
const selectedFontIds = ref<Set<string>>(new Set())
const favoriteFontIds = ref<Set<string>>(readSet('font.favoriteFontIds'))
const previewFontIds = ref<Set<string>>(readSet('font.previewFontIds'))
const expandedCategoryNames = ref<Set<string>>(readSet('font.expandedCategories'))
const fontTree = ref<FontTreeNode[]>([])
const isLoadingFonts = ref(false)
// 计算属性
const selectedFonts = computed(() => {
return fonts.value.filter(f => selectedFontIds.value.has(f.id))
})
const favoriteFonts = computed(() => {
return fonts.value.filter(f => favoriteFontIds.value.has(f.id))
})
const previewFonts = computed(() => {
return fonts.value.filter(f => previewFontIds.value.has(f.id))
})
const favoriteTree = computed(() => {
return buildFontTree(favoriteFonts.value)
})
// 方法
function addFont(fontInfo: FontInfo) {
fonts.value.push(fontInfo)
}
function removeFont(fontId: string) {
const index = fonts.value.findIndex(f => f.id === fontId)
if (index !== -1) {
fonts.value.splice(index, 1)
}
selectedFontIds.value.delete(fontId)
favoriteFontIds.value.delete(fontId)
writeSet('font.favoriteFontIds', favoriteFontIds.value)
previewFontIds.value.delete(fontId)
writeSet('font.previewFontIds', previewFontIds.value)
}
function selectFont(fontId: string) {
selectedFontIds.value.add(fontId)
}
function unselectFont(fontId: string) {
selectedFontIds.value.delete(fontId)
}
function toggleSelectFont(fontId: string) {
if (selectedFontIds.value.has(fontId)) {
unselectFont(fontId)
} else {
selectFont(fontId)
}
}
function clearSelection() {
selectedFontIds.value.clear()
}
function favoriteFont(fontId: string) {
const font = fonts.value.find(f => f.id === fontId)
if (font) {
font.isFavorite = true
favoriteFontIds.value.add(fontId)
writeSet('font.favoriteFontIds', favoriteFontIds.value)
}
}
function unfavoriteFont(fontId: string) {
const font = fonts.value.find(f => f.id === fontId)
if (font) {
font.isFavorite = false
favoriteFontIds.value.delete(fontId)
writeSet('font.favoriteFontIds', favoriteFontIds.value)
}
}
function toggleFavorite(fontId: string) {
if (favoriteFontIds.value.has(fontId)) {
unfavoriteFont(fontId)
} else {
favoriteFont(fontId)
}
}
function addToPreview(fontId: string) {
previewFontIds.value.add(fontId)
writeSet('font.previewFontIds', previewFontIds.value)
}
function removeFromPreview(fontId: string) {
previewFontIds.value.delete(fontId)
writeSet('font.previewFontIds', previewFontIds.value)
}
function togglePreview(fontId: string) {
if (previewFontIds.value.has(fontId)) {
removeFromPreview(fontId)
} else {
addToPreview(fontId)
}
}
function clearPreview() {
previewFontIds.value.clear()
writeSet('font.previewFontIds', previewFontIds.value)
}
function setCategoryExpanded(categoryName: string, expanded: boolean) {
const next = new Set(expandedCategoryNames.value)
if (expanded) {
next.add(categoryName)
} else {
next.delete(categoryName)
}
expandedCategoryNames.value = next
writeSet('font.expandedCategories', expandedCategoryNames.value)
}
async function loadFont(fontInfo: FontInfo) {
if (fontInfo.loaded || !fontInfo.path) {
return
}
try {
const font = await loadFontWithProgress(fontInfo.path, (progress) => {
fontInfo.progress = progress
})
fontInfo.font = font
fontInfo.loaded = true
fontInfo.progress = 100
} catch (error) {
console.error(`Failed to load font ${fontInfo.name}:`, error)
throw error
}
}
function buildFontTree(fontList: FontInfo[]): FontTreeNode[] {
const tree: FontTreeNode[] = []
const categoryMap = new Map<string, FontTreeNode>()
for (const font of fontList) {
let categoryNode = categoryMap.get(font.category)
if (!categoryNode) {
categoryNode = {
name: font.category,
type: 'category',
children: [],
expanded: expandedCategoryNames.value.has(font.category),
selected: false,
}
categoryMap.set(font.category, categoryNode)
tree.push(categoryNode)
}
const fontNode: FontTreeNode = {
name: font.name,
type: 'font',
fontInfo: font,
expanded: false,
selected: selectedFontIds.value.has(font.id),
}
categoryNode.children!.push(fontNode)
}
// 按类别名称排序
tree.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
// 每个类别内的字体按名称排序
for (const category of tree) {
if (category.children) {
category.children.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
}
}
return tree
}
function updateFontTree() {
fontTree.value = buildFontTree(fonts.value)
}
return {
// 状态
fonts,
selectedFontIds,
favoriteFontIds,
previewFontIds,
expandedCategoryNames,
fontTree,
isLoadingFonts,
// 计算属性
selectedFonts,
favoriteFonts,
previewFonts,
favoriteTree,
// 方法
addFont,
removeFont,
selectFont,
unselectFont,
toggleSelectFont,
clearSelection,
favoriteFont,
unfavoriteFont,
toggleFavorite,
addToPreview,
removeFromPreview,
togglePreview,
clearPreview,
setCategoryExpanded,
loadFont,
updateFontTree,
}
})

View File

@@ -0,0 +1,177 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { PreviewItem } from '../types/font'
export const useUiStore = defineStore('ui', () => {
function clampFontSize(size: number) {
return Math.max(10, Math.min(500, size))
}
const initialFontSize = (() => {
try {
const stored = localStorage.getItem('ui.fontSize')
const parsed = stored ? Number(stored) : NaN
return Number.isFinite(parsed) ? clampFontSize(parsed) : 100
} catch {
return 100
}
})()
const initialInputText = (() => {
try {
return localStorage.getItem('ui.inputText') || ''
} catch {
return ''
}
})()
const initialTextColor = (() => {
try {
return localStorage.getItem('ui.textColor') || '#000000'
} catch {
return '#000000'
}
})()
const initialSelectedExportItems = (() => {
try {
const stored = localStorage.getItem('ui.selectedExportItems')
return stored ? JSON.parse(stored) : []
} catch {
return []
}
})()
// 状态
const inputText = ref(initialInputText)
const fontSize = ref(initialFontSize)
const textColor = ref(initialTextColor)
const letterSpacing = ref(0)
const enableLigatures = ref(true)
// 导出相关
const selectedExportItems = ref<PreviewItem[]>(initialSelectedExportItems)
const isExporting = ref(false)
// 侧边栏状态
const isFontSelectorExpanded = ref(true)
const isFavoritesExpanded = ref(true)
// 方法
function setInputText(text: string) {
inputText.value = text
try {
localStorage.setItem('ui.inputText', text)
} catch {
// ignore storage errors
}
}
function setFontSize(size: number) {
const clamped = clampFontSize(size)
fontSize.value = clamped
try {
localStorage.setItem('ui.fontSize', String(clamped))
} catch {
// ignore storage errors
}
}
function setTextColor(color: string) {
textColor.value = color
try {
localStorage.setItem('ui.textColor', color)
} catch {
// ignore storage errors
}
}
function setLetterSpacing(spacing: number) {
letterSpacing.value = spacing
}
function toggleLigatures() {
enableLigatures.value = !enableLigatures.value
}
function toggleExportItem(item: PreviewItem) {
const index = selectedExportItems.value.findIndex(i => i.fontInfo.id === item.fontInfo.id)
if (index >= 0) {
selectedExportItems.value.splice(index, 1)
} else {
selectedExportItems.value.push(item)
}
try {
localStorage.setItem('ui.selectedExportItems', JSON.stringify(selectedExportItems.value))
} catch {
// ignore storage errors
}
}
function retainExportItemsByFontIds(validFontIds: Set<string>) {
const nextItems = selectedExportItems.value.filter(item => validFontIds.has(item.fontInfo.id))
if (nextItems.length === selectedExportItems.value.length) {
return
}
selectedExportItems.value = nextItems
try {
localStorage.setItem('ui.selectedExportItems', JSON.stringify(selectedExportItems.value))
} catch {
// ignore storage errors
}
}
function clearExportSelection() {
selectedExportItems.value = []
try {
localStorage.setItem('ui.selectedExportItems', JSON.stringify([]))
} catch {
// ignore storage errors
}
}
function selectAllExportItems(items: PreviewItem[]) {
selectedExportItems.value = [...items]
try {
localStorage.setItem('ui.selectedExportItems', JSON.stringify(selectedExportItems.value))
} catch {
// ignore storage errors
}
}
function toggleFontSelectorExpanded() {
isFontSelectorExpanded.value = !isFontSelectorExpanded.value
}
function toggleFavoritesExpanded() {
isFavoritesExpanded.value = !isFavoritesExpanded.value
}
return {
// 状态
inputText,
fontSize,
textColor,
letterSpacing,
enableLigatures,
selectedExportItems,
isExporting,
isFontSelectorExpanded,
isFavoritesExpanded,
// 方法
setInputText,
setFontSize,
setTextColor,
setLetterSpacing,
toggleLigatures,
toggleExportItem,
retainExportItemsByFontIds,
clearExportSelection,
selectAllExportItems,
toggleFontSelectorExpanded,
toggleFavoritesExpanded,
}
})