update at 2026-02-08 23:38:19
This commit is contained in:
@@ -8,6 +8,11 @@ const { renderSvgByApi, renderPngByApi } = require('../../utils/mp/render-api')
|
||||
// const { ICON_PATHS } = require('../../config/cdn') // CDN 方案暂时注释
|
||||
|
||||
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配置
|
||||
const LOCAL_ICON_PATHS = {
|
||||
@@ -18,6 +23,7 @@ const LOCAL_ICON_PATHS = {
|
||||
// 导出按钮(根据Figma annotation)
|
||||
exportSvg: '/assets/icons/export-svg-s.svg', // 紫色SVG导出按钮
|
||||
exportPng: '/assets/icons/export-png-s.svg', // 蓝色PNG导出按钮
|
||||
download: '/assets/icons/download.svg', // 下载图标
|
||||
// 输入框图标
|
||||
content: '/assets/icons/content.svg', // 输入框左侧绿色图标
|
||||
// 字体树图标(根据Figma annotation)
|
||||
@@ -43,6 +49,12 @@ function normalizeHexColor(input) {
|
||||
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) {
|
||||
if (!error) {
|
||||
return fallback
|
||||
@@ -69,16 +81,75 @@ function writePngBufferToTempFile(pngBuffer, fontName) {
|
||||
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({
|
||||
data: {
|
||||
inputText: '星程字体转换',
|
||||
fontSize: 120,
|
||||
fontSize: FONT_SIZE_MAX,
|
||||
letterSpacingInput: '0',
|
||||
textColor: '#000000',
|
||||
colorPalette: COLOR_PALETTE,
|
||||
selectedFonts: [], // 当前已选中的字体列表
|
||||
fontCategories: [], // 字体分类树
|
||||
favoriteCategories: [], // 已收藏字体分类树
|
||||
favoriteFonts: [], // 已收藏字体平铺列表
|
||||
showColorPicker: false,
|
||||
favorites: [],
|
||||
// 使用本地图标路径
|
||||
@@ -97,6 +168,7 @@ Page({
|
||||
|
||||
this.fontMap = new Map()
|
||||
this.generateTimer = null
|
||||
this.categoryExpandedMap = {}
|
||||
|
||||
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() {
|
||||
wx.showLoading({ title: '加载中', mask: true })
|
||||
try {
|
||||
@@ -127,12 +206,15 @@ Page({
|
||||
|
||||
this.setData({
|
||||
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:
|
||||
typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput,
|
||||
textColor: normalizeHexColor(state.textColor || this.data.textColor),
|
||||
favorites,
|
||||
})
|
||||
this.categoryExpandedMap = state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
|
||||
? { ...state.categoryExpandedMap }
|
||||
: {}
|
||||
|
||||
// 构建字体树
|
||||
this.updateFontTrees()
|
||||
@@ -164,59 +246,72 @@ Page({
|
||||
// 构建字体分类树
|
||||
updateFontTrees() {
|
||||
const categoryMap = new Map()
|
||||
const favoriteCategoryMap = new Map()
|
||||
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 => {
|
||||
const category = font.category || '其他'
|
||||
const isFavorite = favorites.includes(font.id)
|
||||
const isSelected = selectedIdSet.has(font.id)
|
||||
|
||||
// “选中/选择/已选/xuan” 搜索模式:仅保留已选字体
|
||||
if (selectedOnlyMode && !isSelected) {
|
||||
return
|
||||
}
|
||||
|
||||
// 应用搜索过滤
|
||||
if (keyword) {
|
||||
const matchesSearch = font.name.toLowerCase().includes(keyword) ||
|
||||
category.toLowerCase().includes(keyword)
|
||||
if (nameKeyword) {
|
||||
const matchesSearch = font.name.toLowerCase().includes(nameKeyword) ||
|
||||
category.toLowerCase().includes(nameKeyword)
|
||||
if (!matchesSearch) return
|
||||
}
|
||||
|
||||
// 所有字体树
|
||||
if (!categoryMap.has(category)) {
|
||||
const expandedFromState = this.categoryExpandedMap[category]
|
||||
categoryMap.set(category, {
|
||||
category,
|
||||
expanded: true,
|
||||
expanded: isSearchMode
|
||||
? true
|
||||
: (typeof expandedFromState === 'boolean' ? expandedFromState : false),
|
||||
fonts: [],
|
||||
})
|
||||
}
|
||||
const selectedIds = this.data.selectedFonts.map(f => f.id)
|
||||
categoryMap.get(category).fonts.push({
|
||||
...font,
|
||||
selected: selectedIds.includes(font.id),
|
||||
selected: isSelected,
|
||||
isFavorite,
|
||||
})
|
||||
|
||||
// 已收藏字体树
|
||||
// 已收藏字体平铺列表
|
||||
if (isFavorite) {
|
||||
if (!favoriteCategoryMap.has(category)) {
|
||||
favoriteCategoryMap.set(category, {
|
||||
category,
|
||||
expanded: true,
|
||||
fonts: [],
|
||||
})
|
||||
}
|
||||
favoriteCategoryMap.get(category).fonts.push({
|
||||
favoriteFonts.push({
|
||||
...font,
|
||||
selected: selectedIds.includes(font.id),
|
||||
selected: isSelected,
|
||||
isFavorite: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const fontCategories = Array.from(categoryMap.values())
|
||||
const favoriteCategories = Array.from(favoriteCategoryMap.values())
|
||||
const fontCategories = Array.from(categoryMap.values()).sort((a, b) => a.category.localeCompare(b.category))
|
||||
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({
|
||||
fontCategories,
|
||||
favoriteCategories,
|
||||
favoriteFonts,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -307,20 +402,15 @@ Page({
|
||||
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 })
|
||||
this.categoryExpandedMap[category] = fontCategories[index].expanded
|
||||
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: this.data.textColor,
|
||||
categoryExpandedMap: this.categoryExpandedMap,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -333,29 +423,31 @@ Page({
|
||||
|
||||
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,
|
||||
// 预览固定基础字号,避免字号/颜色调整时重复请求服务端
|
||||
fontSize: PREVIEW_RENDER_FONT_SIZE,
|
||||
fillColor: '#000000',
|
||||
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
|
||||
selectedFonts[index].previewError = ''
|
||||
selectedFonts[index].baseSvg = result.svg
|
||||
selectedFonts[index].baseWidth = result.width
|
||||
selectedFonts[index].baseHeight = result.height
|
||||
selectedFonts[index].renderFontSize = PREVIEW_RENDER_FONT_SIZE
|
||||
selectedFonts[index] = applyLocalStyleToFontItem(
|
||||
selectedFonts[index],
|
||||
Number(this.data.fontSize),
|
||||
this.data.textColor,
|
||||
)
|
||||
this.setData({ selectedFonts })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -365,6 +457,7 @@ Page({
|
||||
if (index >= 0) {
|
||||
selectedFonts[index].previewSrc = ''
|
||||
selectedFonts[index].svg = ''
|
||||
selectedFonts[index].baseSvg = ''
|
||||
selectedFonts[index].previewError = (error && error.message) ? error.message : '预览生成失败'
|
||||
this.setData({ selectedFonts })
|
||||
}
|
||||
@@ -412,23 +505,24 @@ Page({
|
||||
},
|
||||
|
||||
onFontSizeChanging(event) {
|
||||
this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize })
|
||||
this.setData({ fontSize: clampFontSize(event.detail.value, this.data.fontSize) })
|
||||
},
|
||||
|
||||
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({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(f => f.id),
|
||||
fontSize: Number(event.detail.value),
|
||||
fontSize: newSize,
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
this.applyLocalPreviewStyles()
|
||||
},
|
||||
|
||||
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 })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
@@ -437,11 +531,11 @@ Page({
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
this.applyLocalPreviewStyles()
|
||||
},
|
||||
|
||||
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 })
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
@@ -450,7 +544,7 @@ Page({
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
this.applyLocalPreviewStyles()
|
||||
},
|
||||
|
||||
onShowColorPicker() {
|
||||
@@ -467,7 +561,7 @@ Page({
|
||||
|
||||
onColorInput(event) {
|
||||
this.setData({ textColor: event.detail.value || '' })
|
||||
this.scheduleGenerate()
|
||||
this.applyLocalPreviewStyles()
|
||||
},
|
||||
|
||||
onPickColor(event) {
|
||||
@@ -483,7 +577,7 @@ Page({
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: color,
|
||||
})
|
||||
this.scheduleGenerate()
|
||||
this.applyLocalPreviewStyles()
|
||||
},
|
||||
|
||||
onRegenerate() {
|
||||
@@ -568,7 +662,10 @@ Page({
|
||||
if (selectedFonts.length === 1) {
|
||||
const font = selectedFonts[0]
|
||||
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' })
|
||||
} catch (error) {
|
||||
showExportError('导出 SVG 失败', error, '请稍后重试')
|
||||
@@ -622,14 +719,17 @@ Page({
|
||||
async onExportSingleSvg(e) {
|
||||
const fontId = e.currentTarget.dataset.fontId
|
||||
const font = this.data.selectedFonts.find(f => f.id === fontId)
|
||||
|
||||
|
||||
if (!font || !font.svg) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
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' })
|
||||
} catch (error) {
|
||||
showExportError('导出 SVG 失败', error, '请稍后重试')
|
||||
@@ -639,7 +739,7 @@ Page({
|
||||
async onExportSinglePng(e) {
|
||||
const fontId = e.currentTarget.dataset.fontId
|
||||
const font = this.data.selectedFonts.find(f => f.id === fontId)
|
||||
|
||||
|
||||
if (!font || !font.svg) {
|
||||
wx.showToast({ title: '请先生成预览', icon: 'none' })
|
||||
return
|
||||
|
||||
@@ -11,24 +11,29 @@
|
||||
<image
|
||||
class="font-size-icon"
|
||||
src="{{icons.fontSizeDecrease}}"
|
||||
mode="aspectFit"
|
||||
bindtap="onDecreaseFontSize"
|
||||
/>
|
||||
<slider
|
||||
class="font-slider"
|
||||
min="24"
|
||||
max="320"
|
||||
step="1"
|
||||
value="{{fontSize}}"
|
||||
show-value="false"
|
||||
activeColor="#9B6BC2"
|
||||
backgroundColor="#E5E6EB"
|
||||
block-size="18"
|
||||
bindchanging="onFontSizeChanging"
|
||||
bindchange="onFontSizeChange"
|
||||
/>
|
||||
<view class="font-slider-wrap">
|
||||
<view class="font-size-value">{{fontSize}}</view>
|
||||
<slider
|
||||
class="font-slider"
|
||||
min="20"
|
||||
max="120"
|
||||
step="1"
|
||||
value="{{fontSize}}"
|
||||
show-value="{{false}}"
|
||||
activeColor="#9B6BC2"
|
||||
backgroundColor="#E5E6EB"
|
||||
block-size="18"
|
||||
bindchanging="onFontSizeChanging"
|
||||
bindchange="onFontSizeChange"
|
||||
/>
|
||||
</view>
|
||||
<image
|
||||
class="font-size-icon"
|
||||
src="{{icons.fontSizeIncrease}}"
|
||||
mode="aspectFit"
|
||||
bindtap="onIncreaseFontSize"
|
||||
/>
|
||||
</view>
|
||||
@@ -62,6 +67,7 @@
|
||||
<image class="font-icon" src="{{icons.fontIcon}}" />
|
||||
<view class="font-name-text">{{item.name}}</view>
|
||||
<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}}">
|
||||
<image class="export-icon-sm" src="{{icons.exportSvg}}" />
|
||||
</view>
|
||||
@@ -76,6 +82,7 @@
|
||||
class="preview-image"
|
||||
mode="widthFix"
|
||||
src="{{item.previewSrc}}"
|
||||
style="{{item.previewImageStyle}}"
|
||||
/>
|
||||
<view wx:elif="{{item.previewError}}" class="preview-error">{{item.previewError}}</view>
|
||||
<view wx:else class="preview-loading">生成中...</view>
|
||||
@@ -112,15 +119,17 @@
|
||||
src="{{icons.expandIcon}}"
|
||||
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 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
|
||||
wx:for="{{item.fonts}}"
|
||||
wx:for-item="font"
|
||||
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}}" />
|
||||
<view class="font-item-name">{{font.name}}</view>
|
||||
<view class="font-item-actions">
|
||||
@@ -146,39 +155,22 @@
|
||||
<!-- 已收藏字体 -->
|
||||
<view class="favorite-selection">
|
||||
<view class="section-title">已收藏</view>
|
||||
<scroll-view class="font-tree" scroll-y>
|
||||
<view wx:for="{{favoriteCategories}}" wx:key="category" class="font-category">
|
||||
<view class="category-header" bindtap="onToggleFavoriteCategory" data-category="{{item.category}}">
|
||||
<image
|
||||
class="expand-icon"
|
||||
src="{{icons.expandIcon}}"
|
||||
style="transform: rotate({{item.expanded ? '90deg' : '0deg'}})"
|
||||
/>
|
||||
<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>
|
||||
<scroll-view class="font-tree favorite-list" scroll-y>
|
||||
<view wx:for="{{favoriteFonts}}" wx:key="id" class="font-item favorite-font-item">
|
||||
<image class="font-item-icon" src="{{icons.fontIcon}}" />
|
||||
<view class="font-item-name">{{item.name}}</view>
|
||||
<view class="font-item-actions">
|
||||
<view class="font-checkbox" bindtap="onToggleFont" data-font-id="{{item.id}}">
|
||||
<view class="checkbox-wrapper {{item.selected ? 'checked' : ''}}">
|
||||
<image wx:if="{{item.selected}}" class="checkbox-icon-sm" src="{{icons.checkboxChecked}}" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="favorite-btn" bindtap="onToggleFavorite" data-font-id="{{item.id}}">
|
||||
<image class="favorite-icon" src="{{icons.favoriteIcon}}" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!favoriteCategories.length}}" class="empty-favorites">暂无收藏字体</view>
|
||||
<view wx:if="{{!favoriteFonts.length}}" class="empty-favorites">暂无收藏字体</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -34,17 +34,39 @@
|
||||
flex: 1;
|
||||
gap: 4rpx; /* 改为 2rpx 的 gap,小程序 rpx = 物理像素*2 */
|
||||
padding: 0 12rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.font-size-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
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 {
|
||||
flex: 1;
|
||||
height: 4rpx;
|
||||
width: 100%;
|
||||
height: 24rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker-btn {
|
||||
@@ -149,7 +171,14 @@
|
||||
.export-btns-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.export-btn-sm {
|
||||
@@ -163,11 +192,11 @@
|
||||
}
|
||||
|
||||
.export-btn-sm.export-svg-btn {
|
||||
background: #8552A1;
|
||||
background: #E3D6EE;
|
||||
}
|
||||
|
||||
.export-btn-sm.export-png-btn {
|
||||
background: #2420A8;
|
||||
background: #FFE4BA;
|
||||
}
|
||||
|
||||
.export-icon-sm {
|
||||
@@ -207,11 +236,12 @@
|
||||
min-height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.preview-loading {
|
||||
@@ -259,14 +289,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 4rpx 0;
|
||||
height: 56rpx;
|
||||
padding: 0;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.selection-header .section-title {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: 56rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@@ -278,6 +311,7 @@
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
height: 40rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
@@ -311,6 +345,7 @@
|
||||
}
|
||||
|
||||
.font-category {
|
||||
position: relative;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
@@ -322,8 +357,8 @@
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
transition: transform 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -340,6 +375,10 @@
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.select-font-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.font-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -349,6 +388,29 @@
|
||||
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 {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
@@ -357,19 +419,28 @@
|
||||
|
||||
.font-item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 20rpx;
|
||||
color: #86909C;
|
||||
line-height: 28rpx;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.font-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.font-checkbox {
|
||||
width: 21rpx;
|
||||
height: 21rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-icon-sm {
|
||||
@@ -380,11 +451,24 @@
|
||||
.favorite-btn {
|
||||
width: 21rpx;
|
||||
height: 21rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.favorite-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.favorite-list {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.favorite-font-item {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.empty-favorites {
|
||||
|
||||
Reference in New Issue
Block a user