update at 2026-02-08 18:28:39

This commit is contained in:
douboer
2026-02-08 18:28:39 +08:00
parent e2a46e413a
commit 0f5a7f0d85
97 changed files with 22029 additions and 59 deletions

View 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()
},
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "Font2SVG"
}

View File

@@ -0,0 +1,217 @@
<view class="container">
<!-- 顶部导航栏 -->
<view class="header-row">
<view class="logo-container">
<image class="logo" src="{{icons.logo}}" mode="aspectFit" />
</view>
<view class="app-title">TextToSVG</view>
<!-- 字体大小滑块 -->
<view class="font-size-control">
<image
class="font-size-icon"
src="{{icons.fontSizeDecrease}}"
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"
/>
<image
class="font-size-icon"
src="{{icons.fontSizeIncrease}}"
bindtap="onIncreaseFontSize"
/>
</view>
<view class="color-picker-btn" bindtap="onShowColorPicker">
<image class="color-icon" src="{{icons.chooseColor}}" />
</view>
</view>
<!-- 输入栏和导出按钮 -->
<view class="input-row">
<view class="text-input-container">
<input
class="text-input"
value="{{inputText}}"
placeholder="此处输入内容"
bindinput="onInputText"
confirm-type="done"
bindconfirm="onRegenerate"
/>
</view>
<view class="export-buttons">
<view class="export-btn" bindtap="onShowExportOptions">
<image class="export-icon" src="{{icons.export}}" />
</view>
<view class="export-btn" bindtap="onExportSvg">
<image class="export-icon" src="{{icons.exportSvg}}" />
</view>
<view class="export-btn" bindtap="onExportPng">
<image class="export-icon" src="{{icons.exportPng}}" />
</view>
</view>
</view>
<!-- 效果预览区域 -->
<view class="preview-section">
<view class="section-title">效果预览</view>
<scroll-view class="preview-list" scroll-y>
<view wx:for="{{selectedFonts}}" wx:key="id" class="preview-item">
<view class="preview-header">
<image class="font-icon" src="{{icons.fontIcon}}" />
<view class="font-name-text">{{item.name}}</view>
<view class="preview-checkbox" bindtap="onTogglePreviewFont" data-font-id="{{item.id}}">
<view class="checkbox-wrapper {{item.showInPreview ? 'checked' : ''}}">
<image wx:if="{{item.showInPreview}}" class="checkbox-icon" src="{{icons.checkbox}}" />
</view>
</view>
</view>
<view class="preview-content" bindtap="onTogglePreviewFont" data-font-id="{{item.id}}">
<image
wx:if="{{item.previewSrc}}"
class="preview-image"
mode="widthFix"
src="{{item.previewSrc}}"
/>
<view wx:else class="preview-loading">生成中...</view>
</view>
</view>
<view wx:if="{{!selectedFonts.length}}" class="preview-empty">请从下方选择字体</view>
</scroll-view>
</view>
<!-- 字体选择和已收藏字体 -->
<view class="bottom-section">
<!-- 字体选择 -->
<view class="font-selection">
<view class="selection-header">
<view class="section-title">选择</view>
<view class="search-container" wx:if="{{showSearch}}">
<image class="search-icon" src="{{icons.search}}" />
<input
class="search-input"
value="{{searchKeyword}}"
placeholder="搜索字体"
bindinput="onSearchInput"
/>
</view>
<view class="search-toggle" bindtap="onToggleSearch">
<image class="search-icon" src="{{icons.search}}" />
</view>
</view>
<scroll-view class="font-tree" scroll-y>
<view wx:for="{{fontCategories}}" wx:key="category" class="font-category">
<view class="category-header" bindtap="onToggleCategory" data-category="{{item.category}}">
<image
class="expand-icon"
src="{{item.expanded ? icons.collapseIcon : icons.expandIcon}}"
/>
<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.checkbox}}" />
</view>
</view>
<view class="favorite-btn" bindtap="onToggleFavorite" data-font-id="{{font.id}}">
<image
class="favorite-icon"
src="{{icons.favoriteIcon}}"
style="opacity: {{font.isFavorite ? '1' : '0.3'}}"
/>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 已收藏字体 -->
<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="{{item.expanded ? icons.collapseIcon : icons.expandIcon}}"
/>
<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.checkbox}}" />
</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 wx:if="{{!favoriteCategories.length}}" class="empty-favorites">暂无收藏字体</view>
</scroll-view>
</view>
</view>
<!-- 颜色选择器弹窗 -->
<view wx:if="{{showColorPicker}}" class="color-picker-modal" bindtap="onHideColorPicker">
<view class="color-picker-content" catchtap="onStopPropagation">
<view class="color-palette">
<view
wx:for="{{colorPalette}}"
wx:key="*this"
class="color-dot"
style="background: {{item}}; border: {{textColor === item ? '3rpx solid #9B6BC2' : '2rpx solid rgba(0,0,0,0.1)'}}"
data-color="{{item}}"
bindtap="onPickColor"
/>
</view>
<view class="color-input-row">
<text>自定义颜色:</text>
<input
class="color-input"
type="text"
maxlength="7"
value="{{textColor}}"
bindinput="onColorInput"
/>
</view>
</view>
</view>
<canvas id="exportCanvas" canvas-id="exportCanvas" type="2d" class="hidden-canvas" />
</view>

View File

@@ -0,0 +1,426 @@
/* 顶部导航栏 */
.header-row {
display: flex;
align-items: center;
gap: 16rpx;
height: 96rpx;
padding: 0 16rpx;
background: #fff;
}
.logo-container {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
overflow: hidden;
flex-shrink: 0;
}
.logo {
width: 100%;
height: 100%;
}
.app-title {
font-size: 32rpx;
font-weight: 600;
color: #8552A1;
flex-shrink: 0;
}
.font-size-control {
display: flex;
align-items: center;
flex: 1;
gap: 4rpx; /* 改为 2rpx 的 gap小程序 rpx = 物理像素*2 */
padding: 0 12rpx;
}
.font-size-icon {
width: 24rpx;
height: 24rpx;
flex-shrink: 0;
}
.font-slider {
flex: 1;
height: 4rpx;
}
.color-picker-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.color-icon {
width: 48rpx;
height: 48rpx;
}
/* 输入栏和导出按钮 */
.input-row {
display: flex;
align-items: center;
gap: 6rpx;
height: 78rpx;
padding: 0 16rpx;
margin-top: 16rpx;
}
.text-input-container {
flex: 1;
height: 100%;
background: #F7F8FA;
border-radius: 12rpx;
padding: 0 6rpx;
}
.text-input {
width: 100%;
height: 100%;
font-size: 20rpx;
color: #4E5969;
}
.export-buttons {
display: flex;
align-items: center;
gap: 12rpx;
height: 100%;
background: #fff;
border: 1rpx solid #E5E6EB;
border-radius: 10rpx;
padding: 5rpx 10rpx;
}
.export-btn {
width: 62rpx;
height: 62rpx;
display: flex;
align-items: center;
justify-content: center;
}
.export-icon {
width: 100%;
height: 100%;
}
/* 效果预览区域 */
.preview-section {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 16rpx;
padding: 0 16rpx;
border: 1rpx solid #f7e0e0;
border-radius: 12rpx;
background: #fff;
overflow: hidden;
}
.section-title {
padding: 12rpx 0;
font-size: 28rpx;
font-weight: 400;
color: #000;
}
.preview-list {
flex: 1;
min-height: 0;
}
.preview-item {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-bottom: 6rpx;
}
.preview-header {
display: flex;
align-items: center;
gap: 4rpx;
padding-bottom: 4rpx;
border-bottom: 0.5rpx solid #C9CDD4;
}
.font-icon {
width: 16rpx;
height: 16rpx;
flex-shrink: 0;
}
.font-name-text {
flex: 1;
font-size: 20rpx;
color: #86909C;
}
.preview-checkbox {
width: 14rpx;
height: 14rpx;
}
.checkbox-wrapper {
width: 100%;
height: 100%;
border: 1rpx solid #C9CDD4;
border-radius: 3rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.checkbox-wrapper.checked {
border-color: #9B6BC2;
background: #9B6BC2;
}
.checkbox-icon {
width: 100%;
height: 100%;
}
.preview-content {
background: transparent;
padding: 4rpx 0;
min-height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
width: 100%;
}
.preview-loading {
font-size: 24rpx;
color: #86909C;
}
.preview-empty {
text-align: center;
padding: 80rpx 0;
font-size: 24rpx;
color: #C9CDD4;
}
/* 字体选择和已收藏字体 */
.bottom-section {
display: flex;
gap: 16rpx;
height: 600rpx;
margin-top: 16rpx;
padding: 0 16rpx;
}
.font-selection,
.favorite-selection {
flex: 1;
display: flex;
flex-direction: column;
border: 1rpx solid #f7e0e0;
border-radius: 16rpx;
background: #fff;
padding: 9rpx;
overflow: hidden;
}
/* 搜索相关样式 */
.selection-header {
display: flex;
align-items: center;
gap: 8rpx;
padding: 4rpx 0;
}
.search-container {
flex: 1;
display: flex;
align-items: center;
gap: 6rpx;
background: #F7F8FA;
border-radius: 8rpx;
padding: 4rpx 12rpx;
height: 56rpx;
}
.search-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
opacity: 0.5;
}
.search-input {
flex: 1;
font-size: 22rpx;
color: #4E5969;
}
.search-toggle {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #F7F8FA;
border-radius: 8rpx;
}
.font-tree {
flex: 1;
min-height: 0;
}
.font-category {
margin-bottom: 14rpx;
}
.category-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 14rpx;
}
.expand-icon {
width: 20rpx;
height: 20rpx;
transition: transform 0.3s;
flex-shrink: 0;
}
.category-name {
font-size: 21rpx;
font-weight: 500;
color: #000;
}
.font-list {
display: flex;
flex-direction: column;
gap: 0;
}
.font-item {
display: flex;
align-items: center;
gap: 9rpx;
padding-bottom: 9rpx;
border-bottom: 1rpx solid #C9CDD4;
margin-bottom: 9rpx;
}
.font-item-icon {
width: 18rpx;
height: 18rpx;
flex-shrink: 0;
}
.font-item-name {
flex: 1;
font-size: 20rpx;
color: #86909C;
}
.font-item-actions {
display: flex;
align-items: center;
gap: 9rpx;
}
.font-checkbox {
width: 21rpx;
height: 21rpx;
}
.checkbox-icon-sm {
width: 100%;
height: 100%;
}
.favorite-btn {
width: 21rpx;
height: 21rpx;
}
.favorite-icon {
width: 100%;
height: 100%;
}
.empty-favorites {
text-align: center;
padding: 60rpx 0;
font-size: 24rpx;
color: #C9CDD4;
}
/* 颜色选择器弹窗 */
.color-picker-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.color-picker-content {
background: #fff;
border-radius: 20rpx;
padding: 40rpx;
width: 80%;
}
.color-palette {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 30rpx;
}
.color-dot {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
box-sizing: border-box;
}
.color-input-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.color-input {
flex: 1;
height: 60rpx;
background: #F7F8FA;
border-radius: 10rpx;
padding: 0 16rpx;
font-size: 28rpx;
}
/* 画布 */
.hidden-canvas {
position: fixed;
width: 1px;
height: 1px;
left: -9999px;
top: -9999px;
}