update at 2026-02-08 18:28:39
This commit is contained in:
605
miniprogram/pages/index/index.js
Normal file
605
miniprogram/pages/index/index.js
Normal file
@@ -0,0 +1,605 @@
|
||||
const { loadFontsManifest } = require('../../utils/mp/font-loader')
|
||||
const { loadAppState, saveAppState, loadFavorites, saveFavorites } = require('../../utils/mp/storage')
|
||||
const { saveSvgToUserPath, shareLocalFile, buildFilename } = require('../../utils/mp/file-export')
|
||||
const { exportSvgToPngByCanvas, savePngToAlbum } = require('../../utils/mp/canvas-export')
|
||||
const { renderSvgByApi } = require('../../utils/mp/render-api')
|
||||
// const { ICON_PATHS } = require('../../config/cdn') // CDN 方案暂时注释
|
||||
|
||||
const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#7c3aed']
|
||||
|
||||
// 临时使用本地图标
|
||||
const LOCAL_ICON_PATHS = {
|
||||
logo: '/assets/icons/webicon.png',
|
||||
fontSizeDecrease: '/assets/icons/font-size-decrease.png',
|
||||
fontSizeIncrease: '/assets/icons/font-size-increase.png',
|
||||
chooseColor: '/assets/icons/choose-color.png',
|
||||
export: '/assets/icons/export.png',
|
||||
exportSvg: '/assets/icons/export-svg.png',
|
||||
exportPng: '/assets/icons/export-png.png',
|
||||
// 字体树图标(参考web项目)
|
||||
fontIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_18.svg', // 字体item图标
|
||||
expandIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_12.svg', // 展开分类图标
|
||||
collapseIcon: 'https://fonts.biboer.cn/assets/icons/zhedie.svg', // 折叠分类图标
|
||||
favoriteIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_19.svg', // 收藏图标
|
||||
checkbox: '/assets/icons/checkbox.png', // 预览checkbox
|
||||
search: 'https://fonts.biboer.cn/assets/icons/search.svg' // 搜索图标
|
||||
}
|
||||
|
||||
function toSvgDataUri(svg) {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
||||
function normalizeHexColor(input) {
|
||||
const fallback = '#000000'
|
||||
const value = String(input || '').trim()
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
inputText: '星程字体转换',
|
||||
fontSize: 120,
|
||||
letterSpacingInput: '0',
|
||||
textColor: '#000000',
|
||||
colorPalette: COLOR_PALETTE,
|
||||
selectedFonts: [], // 当前已选中的字体列表
|
||||
fontCategories: [], // 字体分类树
|
||||
favoriteCategories: [], // 已收藏字体分类树
|
||||
showColorPicker: false,
|
||||
favorites: [],
|
||||
// 使用本地图标路径
|
||||
icons: LOCAL_ICON_PATHS,
|
||||
// 搜索功能
|
||||
searchKeyword: '',
|
||||
showSearch: false,
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
// 调试:打印图标配置
|
||||
console.log('========== 图标配置 ==========')
|
||||
console.log('icons:', this.data.icons)
|
||||
console.log('logo URL:', this.data.icons.logo)
|
||||
console.log('============================')
|
||||
|
||||
this.fontMap = new Map()
|
||||
this.generateTimer = null
|
||||
|
||||
await this.bootstrap()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const favorites = loadFavorites()
|
||||
this.setData({ favorites })
|
||||
this.updateFontTrees()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.generateTimer) {
|
||||
clearTimeout(this.generateTimer)
|
||||
this.generateTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
async bootstrap() {
|
||||
wx.showLoading({ title: '加载中', mask: true })
|
||||
try {
|
||||
const state = loadAppState()
|
||||
const fonts = await loadFontsManifest()
|
||||
const favorites = loadFavorites()
|
||||
|
||||
for (const font of fonts) {
|
||||
this.fontMap.set(font.id, font)
|
||||
}
|
||||
|
||||
this.setData({
|
||||
inputText: state.inputText || this.data.inputText,
|
||||
fontSize: Number(state.fontSize) > 0 ? Number(state.fontSize) : this.data.fontSize,
|
||||
letterSpacingInput:
|
||||
typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput,
|
||||
textColor: normalizeHexColor(state.textColor || this.data.textColor),
|
||||
favorites,
|
||||
})
|
||||
|
||||
// 构建字体树
|
||||
this.updateFontTrees()
|
||||
|
||||
// 如果有保存的选中字体,恢复它们
|
||||
if (state.selectedFontIds && state.selectedFontIds.length > 0) {
|
||||
const selectedFonts = state.selectedFontIds
|
||||
.map(id => this.fontMap.get(id))
|
||||
.filter(font => font)
|
||||
.map(font => ({
|
||||
id: font.id,
|
||||
name: font.name,
|
||||
category: font.category,
|
||||
showInPreview: true,
|
||||
previewSrc: '',
|
||||
}))
|
||||
|
||||
this.setData({ selectedFonts })
|
||||
await this.generateAllPreviews()
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : '初始化失败'
|
||||
wx.showToast({ title: message, icon: 'none', duration: 2200 })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
// 构建字体分类树
|
||||
updateFontTrees() {
|
||||
const categoryMap = new Map()
|
||||
const favoriteCategoryMap = new Map()
|
||||
const favorites = this.data.favorites
|
||||
const keyword = (this.data.searchKeyword || '').trim().toLowerCase()
|
||||
|
||||
this.fontMap.forEach(font => {
|
||||
const category = font.category || '其他'
|
||||
const isFavorite = favorites.includes(font.id)
|
||||
|
||||
// 应用搜索过滤
|
||||
if (keyword) {
|
||||
const matchesSearch = font.name.toLowerCase().includes(keyword) ||
|
||||
category.toLowerCase().includes(keyword)
|
||||
if (!matchesSearch) return
|
||||
}
|
||||
|
||||
// 所有字体树
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, {
|
||||
category,
|
||||
expanded: true,
|
||||
fonts: [],
|
||||
})
|
||||
}
|
||||
const selectedIds = this.data.selectedFonts.map(f => f.id)
|
||||
categoryMap.get(category).fonts.push({
|
||||
...font,
|
||||
selected: selectedIds.includes(font.id),
|
||||
isFavorite,
|
||||
})
|
||||
|
||||
// 已收藏字体树
|
||||
if (isFavorite) {
|
||||
if (!favoriteCategoryMap.has(category)) {
|
||||
favoriteCategoryMap.set(category, {
|
||||
category,
|
||||
expanded: true,
|
||||
fonts: [],
|
||||
})
|
||||
}
|
||||
favoriteCategoryMap.get(category).fonts.push({
|
||||
...font,
|
||||
selected: selectedIds.includes(font.id),
|
||||
isFavorite: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const fontCategories = Array.from(categoryMap.values())
|
||||
const favoriteCategories = Array.from(favoriteCategoryMap.values())
|
||||
|
||||
this.setData({
|
||||
fontCategories,
|
||||
favoriteCategories,
|
||||
})
|
||||
},
|
||||
|
||||
// 切换字体选择
|
||||
async onToggleFont(e) {
|
||||
const fontId = e.currentTarget.dataset.fontId
|
||||
if (!fontId) return
|
||||
|
||||
const font = this.fontMap.get(fontId)
|
||||
if (!font) return
|
||||
|
||||
const selectedFonts = this.data.selectedFonts
|
||||
const index = selectedFonts.findIndex(f => f.id === fontId)
|
||||
|
||||
if (index >= 0) {
|
||||
// 取消选择
|
||||
selectedFonts.splice(index, 1)
|
||||
} else {
|
||||
// 添加选择
|
||||
selectedFonts.push({
|
||||
id: font.id,
|
||||
name: font.name,
|
||||
category: font.category,
|
||||
showInPreview: true,
|
||||
previewSrc: '',
|
||||
})
|
||||
}
|
||||
|
||||
this.setData({ selectedFonts })
|
||||
this.updateFontTrees()
|
||||
|
||||
// 保存状态
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: selectedFonts.map(f => f.id),
|
||||
fontSize: Number(this.data.fontSize),
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
|
||||
// 如果是新选中的字体,生成预览
|
||||
if (index < 0) {
|
||||
await this.generatePreviewForFont(fontId)
|
||||
}
|
||||
},
|
||||
|
||||
// 切换字体在预览中的显示
|
||||
onTogglePreviewFont(e) {
|
||||
const fontId = e.currentTarget.dataset.fontId
|
||||
if (!fontId) return
|
||||
|
||||
const selectedFonts = this.data.selectedFonts
|
||||
const index = selectedFonts.findIndex(f => f.id === fontId)
|
||||
|
||||
if (index >= 0) {
|
||||
selectedFonts[index].showInPreview = !selectedFonts[index].showInPreview
|
||||
this.setData({ selectedFonts })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换收藏
|
||||
onToggleFavorite(e) {
|
||||
const fontId = e.currentTarget.dataset.fontId
|
||||
if (!fontId) return
|
||||
|
||||
let favorites = [...this.data.favorites]
|
||||
const index = favorites.indexOf(fontId)
|
||||
|
||||
if (index >= 0) {
|
||||
favorites.splice(index, 1)
|
||||
} else {
|
||||
favorites.push(fontId)
|
||||
}
|
||||
|
||||
this.setData({ favorites })
|
||||
saveFavorites(favorites)
|
||||
this.updateFontTrees()
|
||||
},
|
||||
|
||||
// 切换分类展开/收起
|
||||
onToggleCategory(e) {
|
||||
const category = e.currentTarget.dataset.category
|
||||
if (!category) return
|
||||
|
||||
const fontCategories = this.data.fontCategories
|
||||
const index = fontCategories.findIndex(c => c.category === category)
|
||||
|
||||
if (index >= 0) {
|
||||
fontCategories[index].expanded = !fontCategories[index].expanded
|
||||
this.setData({ fontCategories })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换收藏分类展开/收起
|
||||
onToggleFavoriteCategory(e) {
|
||||
const category = e.currentTarget.dataset.category
|
||||
if (!category) return
|
||||
|
||||
const favoriteCategories = this.data.favoriteCategories
|
||||
const index = favoriteCategories.findIndex(c => c.category === category)
|
||||
|
||||
if (index >= 0) {
|
||||
favoriteCategories[index].expanded = !favoriteCategories[index].expanded
|
||||
this.setData({ favoriteCategories })
|
||||
}
|
||||
},
|
||||
|
||||
// 生成单个字体的预览
|
||||
async generatePreviewForFont(fontId) {
|
||||
const text = String(this.data.inputText || '')
|
||||
if (!text.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const letterSpacing = Number(this.data.letterSpacingInput || 0)
|
||||
const fillColor = normalizeHexColor(this.data.textColor)
|
||||
|
||||
const result = await renderSvgByApi({
|
||||
fontId,
|
||||
text,
|
||||
fontSize: Number(this.data.fontSize),
|
||||
fillColor,
|
||||
letterSpacing,
|
||||
maxCharsPerLine: 45,
|
||||
})
|
||||
|
||||
const previewImageSrc = toSvgDataUri(result.svg)
|
||||
|
||||
// 更新对应字体的预览
|
||||
const selectedFonts = this.data.selectedFonts
|
||||
const index = selectedFonts.findIndex(f => f.id === fontId)
|
||||
|
||||
if (index >= 0) {
|
||||
selectedFonts[index].previewSrc = previewImageSrc
|
||||
selectedFonts[index].svg = result.svg
|
||||
selectedFonts[index].width = result.width
|
||||
selectedFonts[index].height = result.height
|
||||
this.setData({ selectedFonts })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成预览失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 生成所有选中字体的预览
|
||||
async generateAllPreviews() {
|
||||
const text = String(this.data.inputText || '')
|
||||
if (!text.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '生成预览中', mask: true })
|
||||
|
||||
try {
|
||||
for (const font of this.data.selectedFonts) {
|
||||
await this.generatePreviewForFont(font.id)
|
||||
}
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
scheduleGenerate() {
|
||||
if (this.generateTimer) {
|
||||
clearTimeout(this.generateTimer)
|
||||
}
|
||||
|
||||
this.generateTimer = setTimeout(() => {
|
||||
this.generateAllPreviews()
|
||||
}, 260)
|
||||
},
|
||||
|
||||
onInputText(event) {
|
||||
this.setData({ inputText: event.detail.value || '' })
|
||||
saveAppState({
|
||||
inputText: event.detail.value || '',
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: Number(this.data.fontSize),
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onFontSizeChanging(event) {
|
||||
this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize })
|
||||
},
|
||||
|
||||
onFontSizeChange(event) {
|
||||
this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: Number(event.detail.value),
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onDecreaseFontSize() {
|
||||
const newSize = Math.max(24, this.data.fontSize - 10)
|
||||
this.setData({ fontSize: newSize })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: newSize,
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onIncreaseFontSize() {
|
||||
const newSize = Math.min(320, this.data.fontSize + 10)
|
||||
this.setData({ fontSize: newSize })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: newSize,
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onShowColorPicker() {
|
||||
this.setData({ showColorPicker: true })
|
||||
},
|
||||
|
||||
onHideColorPicker() {
|
||||
this.setData({ showColorPicker: false })
|
||||
},
|
||||
|
||||
onStopPropagation() {
|
||||
// 阻止事件冒泡
|
||||
},
|
||||
|
||||
onColorInput(event) {
|
||||
this.setData({ textColor: event.detail.value || '' })
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onPickColor(event) {
|
||||
const color = event.currentTarget.dataset.color
|
||||
if (!color) {
|
||||
return
|
||||
}
|
||||
this.setData({ textColor: color })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: Number(this.data.fontSize),
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: color,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
},
|
||||
|
||||
onRegenerate() {
|
||||
this.generateAllPreviews()
|
||||
},
|
||||
|
||||
onShowExportOptions() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['导出全部为 SVG', '导出全部为 PNG'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.exportAllSvg()
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.exportAllPng()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async exportAllSvg() {
|
||||
const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg)
|
||||
|
||||
if (selectedFonts.length === 0) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '导出 SVG 中', mask: true })
|
||||
|
||||
try {
|
||||
for (const font of selectedFonts) {
|
||||
const filePath = await saveSvgToUserPath(font.svg, font.name, this.data.inputText)
|
||||
const filename = buildFilename(font.name, this.data.inputText, 'svg')
|
||||
await shareLocalFile(filePath, filename)
|
||||
}
|
||||
wx.showToast({ title: 'SVG 导出完成', icon: 'success' })
|
||||
} catch (error) {
|
||||
const message = error && error.errMsg ? error.errMsg : error.message
|
||||
wx.showToast({ title: message || '导出 SVG 失败', icon: 'none', duration: 2400 })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
async exportAllPng() {
|
||||
const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg)
|
||||
|
||||
if (selectedFonts.length === 0) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '导出 PNG 中', mask: true })
|
||||
|
||||
try {
|
||||
for (const font of selectedFonts) {
|
||||
const width = Math.max(64, Math.round(font.width || 1024))
|
||||
const height = Math.max(64, Math.round(font.height || 1024))
|
||||
|
||||
const pngPath = await exportSvgToPngByCanvas(this, {
|
||||
svgString: font.svg,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
||||
await savePngToAlbum(pngPath)
|
||||
}
|
||||
wx.showToast({ title: 'PNG 已保存到相册', icon: 'success' })
|
||||
} catch (error) {
|
||||
const message = error && error.errMsg ? error.errMsg : error.message
|
||||
wx.showToast({ title: message || '导出 PNG 失败', icon: 'none', duration: 2400 })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
async onExportSvg() {
|
||||
const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg)
|
||||
|
||||
if (selectedFonts.length === 0) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 如果只有一个字体,直接导出
|
||||
if (selectedFonts.length === 1) {
|
||||
const font = selectedFonts[0]
|
||||
wx.showLoading({ title: '导出 SVG 中', mask: true })
|
||||
try {
|
||||
const filePath = await saveSvgToUserPath(font.svg, font.name, this.data.inputText)
|
||||
const filename = buildFilename(font.name, this.data.inputText, 'svg')
|
||||
await shareLocalFile(filePath, filename)
|
||||
wx.showToast({ title: 'SVG 已分享', icon: 'success' })
|
||||
} catch (error) {
|
||||
const message = error && error.errMsg ? error.errMsg : error.message
|
||||
wx.showToast({ title: message || '导出 SVG 失败', icon: 'none', duration: 2400 })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
} else {
|
||||
this.exportAllSvg()
|
||||
}
|
||||
},
|
||||
|
||||
async onExportPng() {
|
||||
const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg)
|
||||
|
||||
if (selectedFonts.length === 0) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 如果只有一个字体,直接导出
|
||||
if (selectedFonts.length === 1) {
|
||||
const font = selectedFonts[0]
|
||||
wx.showLoading({ title: '导出 PNG 中', mask: true })
|
||||
|
||||
try {
|
||||
const width = Math.max(64, Math.round(font.width || 1024))
|
||||
const height = Math.max(64, Math.round(font.height || 1024))
|
||||
|
||||
const pngPath = await exportSvgToPngByCanvas(this, {
|
||||
svgString: font.svg,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
||||
const saveResult = await savePngToAlbum(pngPath)
|
||||
if (saveResult.success) {
|
||||
wx.showToast({ title: 'PNG 已保存到相册', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error && error.errMsg ? error.errMsg : error.message
|
||||
wx.showToast({ title: message || '导出 PNG 失败', icon: 'none', duration: 2400 })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
} else {
|
||||
this.exportAllPng()
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索功能
|
||||
onToggleSearch() {
|
||||
this.setData({ showSearch: !this.data.showSearch })
|
||||
if (!this.data.showSearch) {
|
||||
this.setData({ searchKeyword: '' })
|
||||
this.updateFontTrees()
|
||||
}
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
const keyword = e.detail.value || ''
|
||||
this.setData({ searchKeyword: keyword })
|
||||
this.updateFontTrees()
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user