update at 2026-02-08 23:38:19

This commit is contained in:
douboer
2026-02-08 23:38:19 +08:00
parent f078dd3261
commit 77a0c7b741
7 changed files with 306 additions and 124 deletions

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
<path fill="#000" d="M8.31 6H6.818V4.364a.273.273 0 0 0-.273-.273h-1.09a.273.273 0 0 0-.273.273v1.639l-1.506.007a.123.123 0 0 0-.114.074l-.001.002c-.003.008-.004.017-.006.026-.001.008-.003.016-.003.025v.005c0 .005.002.01.003.014.002.01.005.021.01.032l.008.014c.005.008.008.016.014.023l2.254 2.518c.004.006.01.009.016.013.003.003.004.007.008.01.003.002.007.002.01.005a.139.139 0 0 0 .028.012l.01.004c.038.011.08.008.11-.017.008-.006.012-.014.017-.022.004-.003.009-.005.012-.009l2.346-2.515c.007-.008.01-.017.016-.026l.007-.011a.123.123 0 0 0 .01-.032c0-.004.004-.008.004-.013v-.013A.121.121 0 0 0 8.31 6ZM5.455 3.818h1.09a.272.272 0 0 0 .273-.272v-.273A.273.273 0 0 0 6.545 3h-1.09a.273.273 0 0 0-.273.273v.273c0 .15.122.272.273.272ZM6 0a6 6 0 1 0 0 12A6 6 0 0 0 6 0Zm0 10.91a4.909 4.909 0 1 1 0-9.82 4.909 4.909 0 0 1 0 9.818Z"/>
</svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12"> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12">
<rect width="25" height="11.908" fill="#2420A8" rx="4"/> <rect width="25" height="11.908" fill="#FFE4BA" rx="4"/>
<path fill="#F3EDF7" d="M3 9.908V2h2.762c.535 0 .94.033 1.216.1.275.067.539.199.79.396.685.535 1.027 1.357 1.027 2.466 0 .842-.252 1.523-.755 2.042a2.27 2.27 0 0 1-.868.567c-.326.118-.726.177-1.198.177H4.582v2.16H3Zm2.656-6.68H4.582V6.52h.967c.512 0 .886-.11 1.122-.33.33-.291.495-.74.495-1.346 0-.519-.13-.918-.39-1.198-.259-.279-.633-.419-1.12-.419Zm4.296 4.52V2h3.045c.456 0 .805.033 1.044.1.24.067.459.187.655.36.402.37.602.952.602 1.747v3.54h-1.581V4.03c0-.283-.063-.488-.189-.614s-.334-.189-.626-.189h-1.369v4.52H9.952ZM19.335 2h2.95v5.713c0 .322-.037.61-.111.861a1.802 1.802 0 0 1-.325.637c-.181.22-.4.372-.655.455-.256.082-.628.124-1.116.124h-3.15V8.562h2.938c.346 0 .573-.053.679-.159.106-.106.16-.325.16-.655h-1.453c-.464 0-.848-.05-1.15-.148a2.173 2.173 0 0 1-.82-.49c-.575-.542-.862-1.29-.862-2.242 0-1.086.35-1.877 1.05-2.372.252-.181.518-.31.797-.384.28-.075.635-.112 1.068-.112Zm1.37 4.52V3.227h-1.311c-.913 0-1.37.551-1.37 1.653 0 .535.125.942.373 1.221.248.28.608.42 1.08.42h1.227Z"/> <path fill="#1D2129" d="M3 9.908V2h2.762c.535 0 .94.033 1.216.1.275.067.539.199.79.396.685.535 1.027 1.357 1.027 2.466 0 .842-.252 1.523-.755 2.042a2.27 2.27 0 0 1-.868.567c-.326.118-.726.177-1.198.177H4.582v2.16H3Zm2.656-6.68H4.582V6.52h.967c.512 0 .886-.11 1.122-.33.33-.291.495-.74.495-1.346 0-.519-.13-.918-.39-1.198-.259-.279-.633-.419-1.12-.419Zm4.296 4.52V2h3.045c.456 0 .805.033 1.044.1.24.067.459.187.655.36.402.37.602.952.602 1.747v3.54h-1.581V4.03c0-.283-.063-.488-.189-.614s-.334-.189-.626-.189h-1.369v4.52H9.952ZM19.335 2h2.95v5.713c0 .322-.037.61-.111.861a1.802 1.802 0 0 1-.325.638c-.181.22-.4.371-.655.454-.256.083-.628.124-1.116.124h-3.15V8.562h2.938c.346 0 .573-.053.679-.159.106-.106.16-.325.16-.655h-1.453c-.464 0-.848-.05-1.15-.148a2.173 2.173 0 0 1-.82-.49c-.575-.542-.862-1.29-.862-2.242 0-1.086.35-1.877 1.05-2.372a2.44 2.44 0 0 1 .797-.384c.28-.075.635-.112 1.068-.112Zm1.37 4.52V3.229h-1.311c-.913 0-1.37.55-1.37 1.652 0 .535.125.942.373 1.221.248.28.608.42 1.08.42h1.227Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12"> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12">
<rect width="25" height="11.921" fill="#8552A1" rx="4"/> <rect width="25" height="11.921" fill="#E3D6EE" rx="4"/>
<path fill="#F3EDF7" d="M4.968 2H8.4v1.248H5.556c-.328 0-.536.016-.624.048-.184.064-.276.22-.276.468 0 .208.088.368.264.48.096.064.356.096.78.096h.948c.608 0 1.088.128 1.44.384.392.288.588.712.588 1.272 0 .424-.12.812-.36 1.164-.176.28-.39.464-.642.552-.252.088-.674.132-1.266.132H3.084V6.596h2.868c.352 0 .592-.004.72-.012.288-.032.432-.196.432-.492 0-.24-.096-.404-.288-.492-.096-.048-.328-.072-.696-.072h-.972c-.384 0-.678-.024-.882-.072a1.575 1.575 0 0 1-.57-.264 1.542 1.542 0 0 1-.51-.63A2.019 2.019 0 0 1 3 3.704c0-.584.212-1.048.636-1.392.256-.208.7-.312 1.332-.312Zm5.995 0 1.488 4.02L14.083 2h1.704l-2.52 5.844h-1.728L9.21 2h1.752Zm8.322 0h3v5.808c0 .328-.037.62-.113.876a1.834 1.834 0 0 1-.33.648 1.412 1.412 0 0 1-.666.462c-.26.084-.638.126-1.134.126h-3.205V8.672h2.989c.351 0 .581-.054.69-.162.108-.108.162-.33.162-.666H19.2c-.472 0-.862-.05-1.17-.15a2.21 2.21 0 0 1-.834-.498c-.584-.552-.876-1.312-.876-2.28 0-1.104.356-1.908 1.068-2.412.256-.184.526-.314.81-.39.284-.076.646-.114 1.086-.114Zm1.392 4.596V3.248h-1.331c-.929 0-1.393.56-1.393 1.68 0 .544.126.958.378 1.242.252.284.618.426 1.099.426h1.247Z"/> <path fill="#1D2129" d="M4.968 2H8.4v1.248H5.556c-.328 0-.536.016-.624.048-.184.064-.276.22-.276.468 0 .208.088.368.264.48.096.064.356.096.78.096h.948c.608 0 1.088.128 1.44.384.392.288.588.712.588 1.272 0 .424-.12.812-.36 1.164-.176.28-.39.464-.642.552-.252.088-.674.132-1.266.132H3.084V6.596h2.868c.352 0 .592-.004.72-.012.288-.032.432-.196.432-.492 0-.24-.096-.404-.288-.492-.096-.048-.328-.072-.696-.072h-.972c-.384 0-.678-.024-.882-.072a1.575 1.575 0 0 1-.57-.264 1.542 1.542 0 0 1-.51-.63A2.019 2.019 0 0 1 3 3.704c0-.584.212-1.048.636-1.392.256-.208.7-.312 1.332-.312Zm5.995 0 1.488 4.02L14.083 2h1.704l-2.52 5.844h-1.728L9.21 2h1.752Zm8.322 0h3v5.808c0 .328-.037.62-.113.876a1.834 1.834 0 0 1-.33.648 1.412 1.412 0 0 1-.666.462c-.26.084-.638.126-1.134.126h-3.205V8.672h2.989c.351 0 .581-.054.69-.162.108-.108.162-.33.162-.666H19.2c-.472 0-.862-.05-1.17-.15a2.21 2.21 0 0 1-.834-.498c-.584-.552-.876-1.312-.876-2.28 0-1.104.356-1.908 1.068-2.412.256-.184.526-.314.81-.39.284-.076.646-.114 1.086-.114Zm1.392 4.596V3.248h-1.331c-.929 0-1.393.56-1.393 1.68 0 .544.126.958.378 1.242.252.284.618.426 1.099.426h1.247Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="10" fill="none" viewBox="0 0 11 10">
<path fill="#FF0D0D" d="M7.549 0C6.645 0 5.807.455 5.25 1.203 4.7.455 3.855 0 2.951 0 1.323 0 0 1.449 0 3.227c0 1.06.473 1.807.856 2.406 1.108 1.742 3.897 3.903 4.017 3.993a.603.603 0 0 0 .754 0c.12-.09 2.904-2.257 4.017-3.993.383-.599.856-1.347.856-2.406C10.5 1.449 9.177 0 7.549 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -8,6 +8,11 @@ const { renderSvgByApi, renderPngByApi } = require('../../utils/mp/render-api')
// const { ICON_PATHS } = require('../../config/cdn') // CDN 方案暂时注释 // const { ICON_PATHS } = require('../../config/cdn') // CDN 方案暂时注释
const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#7c3aed'] const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#7c3aed']
const FONT_SIZE_MIN = 20
const FONT_SIZE_MAX = 120
const PREVIEW_RENDER_FONT_SIZE = 120
const MIN_PREVIEW_IMAGE_WIDTH = 24
const MAX_PREVIEW_IMAGE_WIDTH = 2400
// 临时使用本地图标 - 根据Figma annotation配置 // 临时使用本地图标 - 根据Figma annotation配置
const LOCAL_ICON_PATHS = { const LOCAL_ICON_PATHS = {
@@ -18,6 +23,7 @@ const LOCAL_ICON_PATHS = {
// 导出按钮根据Figma annotation // 导出按钮根据Figma annotation
exportSvg: '/assets/icons/export-svg-s.svg', // 紫色SVG导出按钮 exportSvg: '/assets/icons/export-svg-s.svg', // 紫色SVG导出按钮
exportPng: '/assets/icons/export-png-s.svg', // 蓝色PNG导出按钮 exportPng: '/assets/icons/export-png-s.svg', // 蓝色PNG导出按钮
download: '/assets/icons/download.svg', // 下载图标
// 输入框图标 // 输入框图标
content: '/assets/icons/content.svg', // 输入框左侧绿色图标 content: '/assets/icons/content.svg', // 输入框左侧绿色图标
// 字体树图标根据Figma annotation // 字体树图标根据Figma annotation
@@ -43,6 +49,12 @@ function normalizeHexColor(input) {
return fallback return fallback
} }
function clampFontSize(value, fallback = PREVIEW_RENDER_FONT_SIZE) {
const parsed = Number(value)
const base = Number.isFinite(parsed) ? parsed : Number(fallback)
return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(base)))
}
function extractErrorMessage(error, fallback) { function extractErrorMessage(error, fallback) {
if (!error) { if (!error) {
return fallback return fallback
@@ -69,16 +81,75 @@ function writePngBufferToTempFile(pngBuffer, fontName) {
return filePath return filePath
} }
function formatSvgNumber(value) {
const text = String(Number(value).toFixed(2))
return text.replace(/\.?0+$/, '')
}
function replaceSvgFillColor(svg, color) {
const normalizedColor = normalizeHexColor(color)
const source = String(svg || '')
if (!source) return ''
if (/<g\b[^>]*\sfill="[^"]*"/.test(source)) {
return source.replace(/(<g\b[^>]*\sfill=")[^"]*(")/, `$1${normalizedColor}$2`)
}
return source.replace(/<g\b([^>]*)>/, `<g$1 fill="${normalizedColor}">`)
}
function scaleSvgDimensions(svg, scale) {
const source = String(svg || '')
const safeScale = Number(scale)
if (!source || !Number.isFinite(safeScale) || safeScale <= 0) {
return source
}
return source
.replace(/width="([0-9]+(?:\.[0-9]+)?)"/, (match, width) => {
const scaledWidth = Number(width) * safeScale
return `width="${formatSvgNumber(scaledWidth)}"`
})
.replace(/height="([0-9]+(?:\.[0-9]+)?)"/, (match, height) => {
const scaledHeight = Number(height) * safeScale
return `height="${formatSvgNumber(scaledHeight)}"`
})
}
function applyLocalStyleToFontItem(font, fontSize, color) {
if (!font || !font.baseSvg) {
return font
}
const targetSize = clampFontSize(fontSize, PREVIEW_RENDER_FONT_SIZE)
const renderSize = Number(font.renderFontSize) > 0 ? Number(font.renderFontSize) : PREVIEW_RENDER_FONT_SIZE
const scale = targetSize / renderSize
const styledSvg = scaleSvgDimensions(replaceSvgFillColor(font.baseSvg, color), scale)
const scaledWidth = Number(font.baseWidth) > 0 ? Number(font.baseWidth) * scale : 0
const previewImageWidth = Math.max(
MIN_PREVIEW_IMAGE_WIDTH,
Math.min(MAX_PREVIEW_IMAGE_WIDTH, Math.round(scaledWidth || MIN_PREVIEW_IMAGE_WIDTH)),
)
return {
...font,
svg: styledSvg,
previewSrc: toSvgDataUri(styledSvg),
previewImageStyle: `width:${previewImageWidth}px;max-width:100%;`,
previewError: '',
}
}
Page({ Page({
data: { data: {
inputText: '星程字体转换', inputText: '星程字体转换',
fontSize: 120, fontSize: FONT_SIZE_MAX,
letterSpacingInput: '0', letterSpacingInput: '0',
textColor: '#000000', textColor: '#000000',
colorPalette: COLOR_PALETTE, colorPalette: COLOR_PALETTE,
selectedFonts: [], // 当前已选中的字体列表 selectedFonts: [], // 当前已选中的字体列表
fontCategories: [], // 字体分类树 fontCategories: [], // 字体分类树
favoriteCategories: [], // 已收藏字体分类树 favoriteFonts: [], // 已收藏字体平铺列表
showColorPicker: false, showColorPicker: false,
favorites: [], favorites: [],
// 使用本地图标路径 // 使用本地图标路径
@@ -97,6 +168,7 @@ Page({
this.fontMap = new Map() this.fontMap = new Map()
this.generateTimer = null this.generateTimer = null
this.categoryExpandedMap = {}
await this.bootstrap() await this.bootstrap()
}, },
@@ -114,6 +186,13 @@ Page({
} }
}, },
applyLocalPreviewStyles() {
const fontSize = clampFontSize(this.data.fontSize, PREVIEW_RENDER_FONT_SIZE)
const textColor = normalizeHexColor(this.data.textColor)
const selectedFonts = this.data.selectedFonts.map(font => applyLocalStyleToFontItem(font, fontSize, textColor))
this.setData({ selectedFonts })
},
async bootstrap() { async bootstrap() {
wx.showLoading({ title: '加载中', mask: true }) wx.showLoading({ title: '加载中', mask: true })
try { try {
@@ -127,12 +206,15 @@ Page({
this.setData({ this.setData({
inputText: state.inputText || this.data.inputText, inputText: state.inputText || this.data.inputText,
fontSize: Number(state.fontSize) > 0 ? Number(state.fontSize) : this.data.fontSize, fontSize: clampFontSize(state.fontSize, this.data.fontSize),
letterSpacingInput: letterSpacingInput:
typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput, typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput,
textColor: normalizeHexColor(state.textColor || this.data.textColor), textColor: normalizeHexColor(state.textColor || this.data.textColor),
favorites, favorites,
}) })
this.categoryExpandedMap = state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
? { ...state.categoryExpandedMap }
: {}
// 构建字体树 // 构建字体树
this.updateFontTrees() this.updateFontTrees()
@@ -164,59 +246,72 @@ Page({
// 构建字体分类树 // 构建字体分类树
updateFontTrees() { updateFontTrees() {
const categoryMap = new Map() const categoryMap = new Map()
const favoriteCategoryMap = new Map()
const favorites = this.data.favorites const favorites = this.data.favorites
const keyword = (this.data.searchKeyword || '').trim().toLowerCase() const selectedIdSet = new Set(this.data.selectedFonts.map(f => f.id))
const normalizedKeyword = (this.data.searchKeyword || '').trim().toLowerCase()
const selectedOnlyMode =
normalizedKeyword.includes('选中') ||
normalizedKeyword.includes('选择') ||
normalizedKeyword.includes('已选') ||
normalizedKeyword.includes('xuan')
const nameKeyword = selectedOnlyMode ? '' : normalizedKeyword
const isSearchMode = normalizedKeyword.length > 0
const favoriteFonts = []
this.fontMap.forEach(font => { this.fontMap.forEach(font => {
const category = font.category || '其他' const category = font.category || '其他'
const isFavorite = favorites.includes(font.id) const isFavorite = favorites.includes(font.id)
const isSelected = selectedIdSet.has(font.id)
// “选中/选择/已选/xuan” 搜索模式:仅保留已选字体
if (selectedOnlyMode && !isSelected) {
return
}
// 应用搜索过滤 // 应用搜索过滤
if (keyword) { if (nameKeyword) {
const matchesSearch = font.name.toLowerCase().includes(keyword) || const matchesSearch = font.name.toLowerCase().includes(nameKeyword) ||
category.toLowerCase().includes(keyword) category.toLowerCase().includes(nameKeyword)
if (!matchesSearch) return if (!matchesSearch) return
} }
// 所有字体树 // 所有字体树
if (!categoryMap.has(category)) { if (!categoryMap.has(category)) {
const expandedFromState = this.categoryExpandedMap[category]
categoryMap.set(category, { categoryMap.set(category, {
category, category,
expanded: true, expanded: isSearchMode
? true
: (typeof expandedFromState === 'boolean' ? expandedFromState : false),
fonts: [], fonts: [],
}) })
} }
const selectedIds = this.data.selectedFonts.map(f => f.id)
categoryMap.get(category).fonts.push({ categoryMap.get(category).fonts.push({
...font, ...font,
selected: selectedIds.includes(font.id), selected: isSelected,
isFavorite, isFavorite,
}) })
// 已收藏字体 // 已收藏字体平铺列表
if (isFavorite) { if (isFavorite) {
if (!favoriteCategoryMap.has(category)) { favoriteFonts.push({
favoriteCategoryMap.set(category, {
category,
expanded: true,
fonts: [],
})
}
favoriteCategoryMap.get(category).fonts.push({
...font, ...font,
selected: selectedIds.includes(font.id), selected: isSelected,
isFavorite: true, isFavorite: true,
}) })
} }
}) })
const fontCategories = Array.from(categoryMap.values()) const fontCategories = Array.from(categoryMap.values()).sort((a, b) => a.category.localeCompare(b.category))
const favoriteCategories = Array.from(favoriteCategoryMap.values()) favoriteFonts.sort((a, b) => {
const categoryCompare = String(a.category || '').localeCompare(String(b.category || ''))
if (categoryCompare !== 0) return categoryCompare
return String(a.name || '').localeCompare(String(b.name || ''))
})
this.setData({ this.setData({
fontCategories, fontCategories,
favoriteCategories, favoriteFonts,
}) })
}, },
@@ -307,20 +402,15 @@ Page({
if (index >= 0) { if (index >= 0) {
fontCategories[index].expanded = !fontCategories[index].expanded fontCategories[index].expanded = !fontCategories[index].expanded
this.setData({ fontCategories }) this.setData({ fontCategories })
} this.categoryExpandedMap[category] = fontCategories[index].expanded
}, saveAppState({
inputText: this.data.inputText,
// 切换收藏分类展开/收起 selectedFontIds: this.data.selectedFonts.map(f => f.id),
onToggleFavoriteCategory(e) { fontSize: Number(this.data.fontSize),
const category = e.currentTarget.dataset.category letterSpacing: Number(this.data.letterSpacingInput || 0),
if (!category) return textColor: this.data.textColor,
categoryExpandedMap: this.categoryExpandedMap,
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 })
} }
}, },
@@ -333,29 +423,31 @@ Page({
try { try {
const letterSpacing = Number(this.data.letterSpacingInput || 0) const letterSpacing = Number(this.data.letterSpacingInput || 0)
const fillColor = normalizeHexColor(this.data.textColor)
const result = await renderSvgByApi({ const result = await renderSvgByApi({
fontId, fontId,
text, text,
fontSize: Number(this.data.fontSize), // 预览固定基础字号,避免字号/颜色调整时重复请求服务端
fillColor, fontSize: PREVIEW_RENDER_FONT_SIZE,
fillColor: '#000000',
letterSpacing, letterSpacing,
maxCharsPerLine: 45, maxCharsPerLine: 45,
}) })
const previewImageSrc = toSvgDataUri(result.svg)
// 更新对应字体的预览 // 更新对应字体的预览
const selectedFonts = this.data.selectedFonts const selectedFonts = this.data.selectedFonts
const index = selectedFonts.findIndex(f => f.id === fontId) const index = selectedFonts.findIndex(f => f.id === fontId)
if (index >= 0) { if (index >= 0) {
selectedFonts[index].previewSrc = previewImageSrc selectedFonts[index].baseSvg = result.svg
selectedFonts[index].svg = result.svg selectedFonts[index].baseWidth = result.width
selectedFonts[index].width = result.width selectedFonts[index].baseHeight = result.height
selectedFonts[index].height = result.height selectedFonts[index].renderFontSize = PREVIEW_RENDER_FONT_SIZE
selectedFonts[index].previewError = '' selectedFonts[index] = applyLocalStyleToFontItem(
selectedFonts[index],
Number(this.data.fontSize),
this.data.textColor,
)
this.setData({ selectedFonts }) this.setData({ selectedFonts })
} }
} catch (error) { } catch (error) {
@@ -365,6 +457,7 @@ Page({
if (index >= 0) { if (index >= 0) {
selectedFonts[index].previewSrc = '' selectedFonts[index].previewSrc = ''
selectedFonts[index].svg = '' selectedFonts[index].svg = ''
selectedFonts[index].baseSvg = ''
selectedFonts[index].previewError = (error && error.message) ? error.message : '预览生成失败' selectedFonts[index].previewError = (error && error.message) ? error.message : '预览生成失败'
this.setData({ selectedFonts }) this.setData({ selectedFonts })
} }
@@ -412,23 +505,24 @@ Page({
}, },
onFontSizeChanging(event) { onFontSizeChanging(event) {
this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize }) this.setData({ fontSize: clampFontSize(event.detail.value, this.data.fontSize) })
}, },
onFontSizeChange(event) { onFontSizeChange(event) {
this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize }) const newSize = clampFontSize(event.detail.value, this.data.fontSize)
this.setData({ fontSize: newSize })
saveAppState({ saveAppState({
inputText: this.data.inputText, inputText: this.data.inputText,
selectedFontIds: this.data.selectedFonts.map(f => f.id), selectedFontIds: this.data.selectedFonts.map(f => f.id),
fontSize: Number(event.detail.value), fontSize: newSize,
letterSpacing: Number(this.data.letterSpacingInput || 0), letterSpacing: Number(this.data.letterSpacingInput || 0),
textColor: this.data.textColor, textColor: this.data.textColor,
}) })
this.scheduleGenerate() this.applyLocalPreviewStyles()
}, },
onDecreaseFontSize() { onDecreaseFontSize() {
const newSize = Math.max(24, this.data.fontSize - 10) const newSize = Math.max(FONT_SIZE_MIN, this.data.fontSize - 10)
this.setData({ fontSize: newSize }) this.setData({ fontSize: newSize })
saveAppState({ saveAppState({
inputText: this.data.inputText, inputText: this.data.inputText,
@@ -437,11 +531,11 @@ Page({
letterSpacing: Number(this.data.letterSpacingInput || 0), letterSpacing: Number(this.data.letterSpacingInput || 0),
textColor: this.data.textColor, textColor: this.data.textColor,
}) })
this.scheduleGenerate() this.applyLocalPreviewStyles()
}, },
onIncreaseFontSize() { onIncreaseFontSize() {
const newSize = Math.min(320, this.data.fontSize + 10) const newSize = Math.min(FONT_SIZE_MAX, this.data.fontSize + 10)
this.setData({ fontSize: newSize }) this.setData({ fontSize: newSize })
saveAppState({ saveAppState({
inputText: this.data.inputText, inputText: this.data.inputText,
@@ -450,7 +544,7 @@ Page({
letterSpacing: Number(this.data.letterSpacingInput || 0), letterSpacing: Number(this.data.letterSpacingInput || 0),
textColor: this.data.textColor, textColor: this.data.textColor,
}) })
this.scheduleGenerate() this.applyLocalPreviewStyles()
}, },
onShowColorPicker() { onShowColorPicker() {
@@ -467,7 +561,7 @@ Page({
onColorInput(event) { onColorInput(event) {
this.setData({ textColor: event.detail.value || '' }) this.setData({ textColor: event.detail.value || '' })
this.scheduleGenerate() this.applyLocalPreviewStyles()
}, },
onPickColor(event) { onPickColor(event) {
@@ -483,7 +577,7 @@ Page({
letterSpacing: Number(this.data.letterSpacingInput || 0), letterSpacing: Number(this.data.letterSpacingInput || 0),
textColor: color, textColor: color,
}) })
this.scheduleGenerate() this.applyLocalPreviewStyles()
}, },
onRegenerate() { onRegenerate() {
@@ -568,7 +662,10 @@ Page({
if (selectedFonts.length === 1) { if (selectedFonts.length === 1) {
const font = selectedFonts[0] const font = selectedFonts[0]
try { try {
await shareSvgFromUserTap(font.svg, font.name, this.data.inputText) const exportSvg = font.baseSvg
? applyLocalStyleToFontItem(font, Number(this.data.fontSize), this.data.textColor).svg
: font.svg
await shareSvgFromUserTap(exportSvg, font.name, this.data.inputText)
wx.showToast({ title: 'SVG 已分享', icon: 'success' }) wx.showToast({ title: 'SVG 已分享', icon: 'success' })
} catch (error) { } catch (error) {
showExportError('导出 SVG 失败', error, '请稍后重试') showExportError('导出 SVG 失败', error, '请稍后重试')
@@ -629,7 +726,10 @@ Page({
} }
try { try {
await shareSvgFromUserTap(font.svg, font.name, this.data.inputText) const exportSvg = font.baseSvg
? applyLocalStyleToFontItem(font, Number(this.data.fontSize), this.data.textColor).svg
: font.svg
await shareSvgFromUserTap(exportSvg, font.name, this.data.inputText)
wx.showToast({ title: 'SVG 已分享', icon: 'success' }) wx.showToast({ title: 'SVG 已分享', icon: 'success' })
} catch (error) { } catch (error) {
showExportError('导出 SVG 失败', error, '请稍后重试') showExportError('导出 SVG 失败', error, '请稍后重试')

View File

@@ -11,24 +11,29 @@
<image <image
class="font-size-icon" class="font-size-icon"
src="{{icons.fontSizeDecrease}}" src="{{icons.fontSizeDecrease}}"
mode="aspectFit"
bindtap="onDecreaseFontSize" bindtap="onDecreaseFontSize"
/> />
<slider <view class="font-slider-wrap">
class="font-slider" <view class="font-size-value">{{fontSize}}</view>
min="24" <slider
max="320" class="font-slider"
step="1" min="20"
value="{{fontSize}}" max="120"
show-value="false" step="1"
activeColor="#9B6BC2" value="{{fontSize}}"
backgroundColor="#E5E6EB" show-value="{{false}}"
block-size="18" activeColor="#9B6BC2"
bindchanging="onFontSizeChanging" backgroundColor="#E5E6EB"
bindchange="onFontSizeChange" block-size="18"
/> bindchanging="onFontSizeChanging"
bindchange="onFontSizeChange"
/>
</view>
<image <image
class="font-size-icon" class="font-size-icon"
src="{{icons.fontSizeIncrease}}" src="{{icons.fontSizeIncrease}}"
mode="aspectFit"
bindtap="onIncreaseFontSize" bindtap="onIncreaseFontSize"
/> />
</view> </view>
@@ -62,6 +67,7 @@
<image class="font-icon" src="{{icons.fontIcon}}" /> <image class="font-icon" src="{{icons.fontIcon}}" />
<view class="font-name-text">{{item.name}}</view> <view class="font-name-text">{{item.name}}</view>
<view class="export-btns-inline"> <view class="export-btns-inline">
<image class="download-icon" src="{{icons.download}}" />
<view class="export-btn-sm export-svg-btn" bindtap="onExportSingleSvg" data-font-id="{{item.id}}"> <view class="export-btn-sm export-svg-btn" bindtap="onExportSingleSvg" data-font-id="{{item.id}}">
<image class="export-icon-sm" src="{{icons.exportSvg}}" /> <image class="export-icon-sm" src="{{icons.exportSvg}}" />
</view> </view>
@@ -76,6 +82,7 @@
class="preview-image" class="preview-image"
mode="widthFix" mode="widthFix"
src="{{item.previewSrc}}" src="{{item.previewSrc}}"
style="{{item.previewImageStyle}}"
/> />
<view wx:elif="{{item.previewError}}" class="preview-error">{{item.previewError}}</view> <view wx:elif="{{item.previewError}}" class="preview-error">{{item.previewError}}</view>
<view wx:else class="preview-loading">生成中...</view> <view wx:else class="preview-loading">生成中...</view>
@@ -112,15 +119,17 @@
src="{{icons.expandIcon}}" src="{{icons.expandIcon}}"
style="transform: rotate({{item.expanded ? '90deg' : '0deg'}})" style="transform: rotate({{item.expanded ? '90deg' : '0deg'}})"
/> />
<view class="category-name">{{item.category}}</view> <view class="category-name">{{item.category}}{{item.fonts.length}}</view>
</view> </view>
<view wx:if="{{item.expanded}}" class="font-list"> <view wx:if="{{item.expanded && item.fonts.length > 0}}" class="tree-vertical-line" />
<view wx:if="{{item.expanded}}" class="font-list select-font-list">
<view <view
wx:for="{{item.fonts}}" wx:for="{{item.fonts}}"
wx:for-item="font" wx:for-item="font"
wx:key="id" wx:key="id"
class="font-item" class="font-item select-font-item"
> >
<view class="tree-horizontal-line" />
<image class="font-item-icon" src="{{icons.fontIcon}}" /> <image class="font-item-icon" src="{{icons.fontIcon}}" />
<view class="font-item-name">{{font.name}}</view> <view class="font-item-name">{{font.name}}</view>
<view class="font-item-actions"> <view class="font-item-actions">
@@ -146,39 +155,22 @@
<!-- 已收藏字体 --> <!-- 已收藏字体 -->
<view class="favorite-selection"> <view class="favorite-selection">
<view class="section-title">已收藏</view> <view class="section-title">已收藏</view>
<scroll-view class="font-tree" scroll-y> <scroll-view class="font-tree favorite-list" scroll-y>
<view wx:for="{{favoriteCategories}}" wx:key="category" class="font-category"> <view wx:for="{{favoriteFonts}}" wx:key="id" class="font-item favorite-font-item">
<view class="category-header" bindtap="onToggleFavoriteCategory" data-category="{{item.category}}"> <image class="font-item-icon" src="{{icons.fontIcon}}" />
<image <view class="font-item-name">{{item.name}}</view>
class="expand-icon" <view class="font-item-actions">
src="{{icons.expandIcon}}" <view class="font-checkbox" bindtap="onToggleFont" data-font-id="{{item.id}}">
style="transform: rotate({{item.expanded ? '90deg' : '0deg'}})" <view class="checkbox-wrapper {{item.selected ? 'checked' : ''}}">
/> <image wx:if="{{item.selected}}" class="checkbox-icon-sm" src="{{icons.checkboxChecked}}" />
<view class="category-name">{{item.category}}</view>
</view>
<view wx:if="{{item.expanded}}" class="font-list">
<view
wx:for="{{item.fonts}}"
wx:for-item="font"
wx:key="id"
class="font-item"
>
<image class="font-item-icon" src="{{icons.fontIcon}}" />
<view class="font-item-name">{{font.name}}</view>
<view class="font-item-actions">
<view class="font-checkbox" bindtap="onToggleFont" data-font-id="{{font.id}}">
<view class="checkbox-wrapper {{font.selected ? 'checked' : ''}}">
<image wx:if="{{font.selected}}" class="checkbox-icon-sm" src="{{icons.checkboxChecked}}" />
</view>
</view>
<view class="favorite-btn" bindtap="onToggleFavorite" data-font-id="{{font.id}}">
<image class="favorite-icon" src="{{icons.favoriteIcon}}" />
</view>
</view> </view>
</view> </view>
<view class="favorite-btn" bindtap="onToggleFavorite" data-font-id="{{item.id}}">
<image class="favorite-icon" src="{{icons.favoriteIcon}}" />
</view>
</view> </view>
</view> </view>
<view wx:if="{{!favoriteCategories.length}}" class="empty-favorites">暂无收藏字体</view> <view wx:if="{{!favoriteFonts.length}}" class="empty-favorites">暂无收藏字体</view>
</scroll-view> </scroll-view>
</view> </view>
</view> </view>

View File

@@ -34,17 +34,39 @@
flex: 1; flex: 1;
gap: 4rpx; /* 改为 2rpx 的 gap小程序 rpx = 物理像素*2 */ gap: 4rpx; /* 改为 2rpx 的 gap小程序 rpx = 物理像素*2 */
padding: 0 12rpx; padding: 0 12rpx;
height: 64rpx;
} }
.font-size-icon { .font-size-icon {
width: 24rpx; width: 24rpx;
height: 24rpx; height: 24rpx;
flex-shrink: 0; flex-shrink: 0;
display: block;
}
.font-slider-wrap {
flex: 1;
height: 64rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
position: relative;
}
.font-size-value {
font-size: 20rpx;
color: #4E5969;
line-height: 28rpx;
text-align: center;
margin-bottom: 4rpx;
} }
.font-slider { .font-slider {
flex: 1; width: 100%;
height: 4rpx; height: 24rpx;
margin: 0;
padding: 0;
} }
.color-picker-btn { .color-picker-btn {
@@ -149,7 +171,14 @@
.export-btns-inline { .export-btns-inline {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12rpx; gap: 10rpx;
}
.download-icon {
width: 24rpx;
height: 24rpx;
flex-shrink: 0;
display: block;
} }
.export-btn-sm { .export-btn-sm {
@@ -163,11 +192,11 @@
} }
.export-btn-sm.export-svg-btn { .export-btn-sm.export-svg-btn {
background: #8552A1; background: #E3D6EE;
} }
.export-btn-sm.export-png-btn { .export-btn-sm.export-png-btn {
background: #2420A8; background: #FFE4BA;
} }
.export-icon-sm { .export-icon-sm {
@@ -207,11 +236,12 @@
min-height: 80rpx; min-height: 80rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
} }
.preview-image { .preview-image {
width: 100%; display: block;
max-width: 100%;
} }
.preview-loading { .preview-loading {
@@ -259,14 +289,17 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8rpx; gap: 8rpx;
padding: 4rpx 0; padding: 0;
height: 56rpx; height: 40rpx;
} }
.selection-header .section-title { .selection-header .section-title {
padding: 0; padding: 0;
height: auto; font-size: 28rpx;
line-height: 56rpx; line-height: 40rpx;
display: flex;
align-items: center;
flex-shrink: 0;
} }
.search-container { .search-container {
@@ -278,6 +311,7 @@
border-radius: 8rpx; border-radius: 8rpx;
padding: 4rpx 12rpx; padding: 4rpx 12rpx;
height: 40rpx; height: 40rpx;
min-width: 0;
} }
.search-icon { .search-icon {
@@ -311,6 +345,7 @@
} }
.font-category { .font-category {
position: relative;
margin-bottom: 14rpx; margin-bottom: 14rpx;
} }
@@ -322,8 +357,8 @@
} }
.expand-icon { .expand-icon {
width: 20rpx; width: 40rpx;
height: 20rpx; height: 40rpx;
transition: transform 0.3s; transition: transform 0.3s;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -340,6 +375,10 @@
gap: 0; gap: 0;
} }
.select-font-list {
position: relative;
}
.font-item { .font-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -349,6 +388,29 @@
margin-bottom: 9rpx; margin-bottom: 9rpx;
} }
.select-font-item {
position: relative;
padding-left: 28rpx;
}
.tree-vertical-line {
position: absolute;
left: 19rpx;
top: 42rpx;
bottom: 16rpx;
width: 1rpx;
background: #C9CDD4;
}
.tree-horizontal-line {
position: absolute;
left: 19rpx;
top: 18rpx;
width: 10rpx;
height: 1rpx;
background: #C9CDD4;
}
.font-item-icon { .font-item-icon {
width: 18rpx; width: 18rpx;
height: 18rpx; height: 18rpx;
@@ -357,19 +419,28 @@
.font-item-name { .font-item-name {
flex: 1; flex: 1;
min-width: 0;
font-size: 20rpx; font-size: 20rpx;
color: #86909C; color: #86909C;
line-height: 28rpx;
white-space: normal;
word-break: break-all;
} }
.font-item-actions { .font-item-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 9rpx; gap: 9rpx;
flex-shrink: 0;
} }
.font-checkbox { .font-checkbox {
width: 21rpx; width: 21rpx;
height: 21rpx; height: 21rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
} }
.checkbox-icon-sm { .checkbox-icon-sm {
@@ -380,11 +451,24 @@
.favorite-btn { .favorite-btn {
width: 21rpx; width: 21rpx;
height: 21rpx; height: 21rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
} }
.favorite-icon { .favorite-icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
}
.favorite-list {
padding-top: 8rpx;
}
.favorite-font-item {
padding-left: 0;
} }
.empty-favorites { .empty-favorites {