update at 2026-02-09 16:09:44
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
`apiserver/` 提供微信小程序用的远端渲染接口:
|
||||
- 小程序只上传参数(字体 ID、文字、字号、颜色等)
|
||||
- 服务端读取 `fonts.json` + `fonts/`,生成 SVG/PNG 后返回
|
||||
- 服务端读取字体清单 + 字体目录,生成 SVG/PNG 后返回
|
||||
|
||||
## 1. 启动
|
||||
|
||||
@@ -17,10 +17,10 @@ python3 apiserver/server.py \
|
||||
|
||||
其中 `--static-root` 目录必须包含:
|
||||
|
||||
- `fonts.json`
|
||||
- `fonts/`(字体文件目录)
|
||||
- `fonts/`(统一字体目录)
|
||||
- `miniprogram/assets/fonts.json`(小程序清单)
|
||||
|
||||
如果不传 `--manifest`,默认读取 `<static-root>/fonts.json`。
|
||||
如果不传 `--manifest`,默认优先读取 `<static-root>/miniprogram/assets/fonts.json`,不存在时回退到 `<static-root>/fonts.json`。
|
||||
|
||||
## 2. API
|
||||
|
||||
@@ -34,7 +34,7 @@ python3 apiserver/server.py \
|
||||
|
||||
```json
|
||||
{
|
||||
"fontId": "其他字体/AlimamaDaoLiTi",
|
||||
"fontId": "0001",
|
||||
"text": "星程字体转换",
|
||||
"fontSize": 120,
|
||||
"fillColor": "#000000",
|
||||
@@ -54,7 +54,7 @@ python3 apiserver/server.py \
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"fontId": "其他字体/AlimamaDaoLiTi",
|
||||
"fontId": "0001",
|
||||
"fontName": "AlimamaDaoLiTi",
|
||||
"width": 956.2,
|
||||
"height": 144.3,
|
||||
@@ -100,7 +100,7 @@ sudo systemctl status font2svg-api
|
||||
|
||||
## 5. 约束
|
||||
|
||||
- 字体解析完全基于 `fonts.json`,`fontId` 必须存在。
|
||||
- 字体解析完全基于字体清单(默认 `miniprogram/assets/fonts.json`),字体文件统一从 `<static-root>/fonts/` 读取,`fontId` 必须存在。
|
||||
- 服务端启用 CORS,允许小程序访问。
|
||||
- 不依赖 Flask/FastAPI,使用 Python 标准库 HTTP 服务。
|
||||
- `/api/render-png` 依赖 `node + sharp`(使用 `apiserver/svg_to_png.js` 转换)。
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -164,12 +165,29 @@ class FontCatalog:
|
||||
class RenderHandler(BaseHTTPRequestHandler):
|
||||
catalog = None
|
||||
|
||||
def send_response(self, code, message=None):
|
||||
self._response_status = code
|
||||
super().send_response(code, message)
|
||||
|
||||
def _set_cors_headers(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
self.send_header("Access-Control-Max-Age", "86400")
|
||||
|
||||
def _log_request_timing(self, start_time):
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
status = getattr(self, "_response_status", "-")
|
||||
client_ip = self.client_address[0] if self.client_address else "-"
|
||||
LOGGER.info(
|
||||
"请求完成 method=%s path=%s status=%s duration_ms=%.2f client=%s",
|
||||
self.command,
|
||||
self.path,
|
||||
status,
|
||||
elapsed_ms,
|
||||
client_ip,
|
||||
)
|
||||
|
||||
def _send_json(self, status_code, payload):
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status_code)
|
||||
@@ -240,69 +258,84 @@ class RenderHandler(BaseHTTPRequestHandler):
|
||||
return font_info, result
|
||||
|
||||
def do_OPTIONS(self): # noqa: N802
|
||||
self.send_response(204)
|
||||
self._set_cors_headers()
|
||||
self.end_headers()
|
||||
start_time = time.perf_counter()
|
||||
self._response_status = None
|
||||
try:
|
||||
self.send_response(204)
|
||||
self._set_cors_headers()
|
||||
self.end_headers()
|
||||
finally:
|
||||
self._log_request_timing(start_time)
|
||||
|
||||
def do_GET(self): # noqa: N802
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/healthz":
|
||||
try:
|
||||
data = self.catalog.health()
|
||||
self._send_json(200, {"ok": True, "data": data})
|
||||
except Exception as error:
|
||||
LOGGER.exception("健康检查失败")
|
||||
self._send_json(500, {"ok": False, "error": str(error)})
|
||||
return
|
||||
start_time = time.perf_counter()
|
||||
self._response_status = None
|
||||
try:
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/healthz":
|
||||
try:
|
||||
data = self.catalog.health()
|
||||
self._send_json(200, {"ok": True, "data": data})
|
||||
except Exception as error:
|
||||
LOGGER.exception("健康检查失败")
|
||||
self._send_json(500, {"ok": False, "error": str(error)})
|
||||
return
|
||||
|
||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||
finally:
|
||||
self._log_request_timing(start_time)
|
||||
|
||||
def do_POST(self): # noqa: N802
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path not in ("/api/render-svg", "/api/render-png"):
|
||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||
return
|
||||
|
||||
start_time = time.perf_counter()
|
||||
self._response_status = None
|
||||
try:
|
||||
render_params = self._parse_render_payload()
|
||||
font_info, result = self._render_svg_core(render_params)
|
||||
except KeyError as error:
|
||||
self._send_json(404, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except FileNotFoundError as error:
|
||||
self._send_json(404, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except ValueError as error:
|
||||
self._send_json(400, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except Exception as error:
|
||||
LOGGER.exception("渲染失败")
|
||||
self._send_json(500, {"ok": False, "error": str(error)})
|
||||
return
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path not in ("/api/render-svg", "/api/render-png"):
|
||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||
return
|
||||
|
||||
if parsed.path == "/api/render-png":
|
||||
try:
|
||||
png_bytes = render_png_from_svg(
|
||||
result["svg"],
|
||||
result["width"],
|
||||
result["height"],
|
||||
)
|
||||
render_params = self._parse_render_payload()
|
||||
font_info, result = self._render_svg_core(render_params)
|
||||
except KeyError as error:
|
||||
self._send_json(404, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except FileNotFoundError as error:
|
||||
self._send_json(404, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except ValueError as error:
|
||||
self._send_json(400, {"ok": False, "error": str(error)})
|
||||
return
|
||||
except Exception as error:
|
||||
LOGGER.exception("PNG 渲染失败")
|
||||
LOGGER.exception("渲染失败")
|
||||
self._send_json(500, {"ok": False, "error": str(error)})
|
||||
return
|
||||
|
||||
self._send_binary(200, png_bytes, "image/png")
|
||||
return
|
||||
if parsed.path == "/api/render-png":
|
||||
try:
|
||||
png_bytes = render_png_from_svg(
|
||||
result["svg"],
|
||||
result["width"],
|
||||
result["height"],
|
||||
)
|
||||
except Exception as error:
|
||||
LOGGER.exception("PNG 渲染失败")
|
||||
self._send_json(500, {"ok": False, "error": str(error)})
|
||||
return
|
||||
|
||||
response_data = {
|
||||
"svg": result["svg"],
|
||||
"width": result["width"],
|
||||
"height": result["height"],
|
||||
"fontName": result.get("fontName") or font_info.get("name") or "Unknown",
|
||||
"fontId": render_params["fontId"],
|
||||
}
|
||||
self._send_json(200, {"ok": True, "data": response_data})
|
||||
self._send_binary(200, png_bytes, "image/png")
|
||||
return
|
||||
|
||||
response_data = {
|
||||
"svg": result["svg"],
|
||||
"width": result["width"],
|
||||
"height": result["height"],
|
||||
"fontName": result.get("fontName") or font_info.get("name") or "Unknown",
|
||||
"fontId": render_params["fontId"],
|
||||
}
|
||||
self._send_json(200, {"ok": True, "data": response_data})
|
||||
finally:
|
||||
self._log_request_timing(start_time)
|
||||
|
||||
def log_message(self, format_str, *args):
|
||||
LOGGER.info("%s - %s", self.address_string(), format_str % args)
|
||||
@@ -321,12 +354,12 @@ def main():
|
||||
parser.add_argument(
|
||||
"--static-root",
|
||||
default=os.getenv("FONT2SVG_STATIC_ROOT", os.getcwd()),
|
||||
help="静态资源根目录(包含 fonts/ 与 fonts.json)",
|
||||
help="静态资源根目录(包含 fonts/ 与配置清单)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest",
|
||||
default=os.getenv("FONT2SVG_MANIFEST_PATH", ""),
|
||||
help="字体清单路径,默认使用 <static-root>/fonts.json",
|
||||
help="字体清单路径,默认优先使用 <static-root>/miniprogram/assets/fonts.json,其次 <static-root>/fonts.json",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -336,7 +369,14 @@ def main():
|
||||
)
|
||||
|
||||
static_root = os.path.abspath(args.static_root)
|
||||
manifest_path = args.manifest.strip() or os.path.join(static_root, "fonts.json")
|
||||
if args.manifest.strip():
|
||||
manifest_path = args.manifest.strip()
|
||||
else:
|
||||
manifest_candidates = [
|
||||
os.path.join(static_root, "miniprogram", "assets", "fonts.json"),
|
||||
os.path.join(static_root, "fonts.json"),
|
||||
]
|
||||
manifest_path = next((item for item in manifest_candidates if os.path.isfile(item)), manifest_candidates[0])
|
||||
|
||||
server = build_server(
|
||||
host=args.host,
|
||||
|
||||
11
frontend/public/default.json
Normal file
11
frontend/public/default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"inputText": "星程字体转换",
|
||||
"fontSize": 50,
|
||||
"textColor": "#dc2626",
|
||||
"selectedFontIds": [
|
||||
"0001"
|
||||
],
|
||||
"favoriteFontIds": [
|
||||
"0001"
|
||||
]
|
||||
}
|
||||
@@ -17,10 +17,13 @@ miniprogram/
|
||||
├── pages/
|
||||
│ ├── index/ # 首页:输入、预览、导出
|
||||
│ └── font-picker/ # 字体选择页
|
||||
├── config/
|
||||
│ └── server.js # 远端地址/端口/API 路径统一配置
|
||||
├── utils/
|
||||
│ ├── core/ # 纯算法模块
|
||||
│ └── mp/ # 小程序 API 适配层
|
||||
├── assets/fonts.json # 字体清单(由脚本生成)
|
||||
├── assets/default.json # 首次加载默认配置(内容/颜色/字号/默认字体)
|
||||
├── app.js / app.json / app.wxss
|
||||
└── project.config.json
|
||||
```
|
||||
@@ -34,6 +37,19 @@ miniprogram/
|
||||
5. Nginx 配置 `/api/` 反向代理到渲染服务。
|
||||
6. 编译运行。
|
||||
|
||||
## 服务器配置(换服务器只改一处)
|
||||
|
||||
修改 `miniprogram/config/server.js` 中的 `SERVER_CONFIG`:
|
||||
|
||||
- `protocol`: `https` / `http`
|
||||
- `host`: 服务器域名
|
||||
- `port`: 端口(默认 443/80 可留空)
|
||||
- `apiPrefix`: API 前缀(默认 `/api`)
|
||||
- `fontsManifestPath`: 字体清单路径(默认 `/miniprogram/assets/fonts.json`)
|
||||
- `defaultConfigPath`: 默认配置路径(默认 `/miniprogram/assets/default.json`)
|
||||
|
||||
`app.js` 和 API 调用会自动使用该配置生成完整 URL。
|
||||
|
||||
## 导出说明
|
||||
|
||||
- `SVG`:受微信限制,`shareFileMessage` 需由单次点击直接触发,建议逐个字体导出。
|
||||
@@ -50,6 +66,29 @@ miniprogram/
|
||||
|
||||
如果 `path` 是相对路径(例如 `/fonts/a.ttf`),服务端会根据静态根目录拼接到实际文件路径。
|
||||
|
||||
推荐部署结构:
|
||||
- 字体目录统一放在服务器根目录:`/fonts/`
|
||||
- Web 配置文件独立管理:`/fonts.json`(可选 `/default.json`)
|
||||
- 小程序配置文件独立管理:`/miniprogram/assets/fonts.json`、`/miniprogram/assets/default.json`
|
||||
|
||||
## 首次默认配置(default.json)
|
||||
|
||||
- 默认配置文件与 `fonts.json` 同目录:由 `config/server.js` 自动拼接(默认是 `https://fonts.biboer.cn/miniprogram/assets/default.json`)
|
||||
- 小程序会在首次加载时读取该配置(远端失败则回退本地 `miniprogram/assets/default.js`)
|
||||
- 配置只在首次加载生效,后续始终使用用户本地已保存配置(选择、收藏、颜色、字号、内容)
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"inputText": "星程字体转换",
|
||||
"fontSize": 50,
|
||||
"textColor": "#dc2626",
|
||||
"selectedFontIds": ["0001"],
|
||||
"favoriteFontIds": ["0001"]
|
||||
}
|
||||
```
|
||||
|
||||
## 调试命令(仓库根目录)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const { buildRuntimeConfig } = require('./config/server')
|
||||
|
||||
const runtimeConfig = buildRuntimeConfig()
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
fontsManifestUrl: 'https://fonts.biboer.cn/fonts.json',
|
||||
fontsBaseUrl: 'https://fonts.biboer.cn',
|
||||
svgRenderApiUrl: 'https://fonts.biboer.cn/api/render-svg',
|
||||
...runtimeConfig,
|
||||
apiTimeoutMs: 30000,
|
||||
fonts: null,
|
||||
defaultConfig: null,
|
||||
},
|
||||
})
|
||||
|
||||
19
miniprogram/assets/default.js
Normal file
19
miniprogram/assets/default.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
inputText: '星程字体转换',
|
||||
fontSize: 50,
|
||||
textColor: '#dc2626',
|
||||
selectedFontIds: [
|
||||
'0001',
|
||||
'0003',
|
||||
'0006',
|
||||
'0011',
|
||||
'0015',
|
||||
],
|
||||
favoriteFontIds: [
|
||||
'0001',
|
||||
'0003',
|
||||
'0006',
|
||||
'0011',
|
||||
'0015',
|
||||
],
|
||||
}
|
||||
19
miniprogram/assets/default.json
Normal file
19
miniprogram/assets/default.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"inputText": "星程字体转换",
|
||||
"fontSize": 50,
|
||||
"textColor": "#dc2626",
|
||||
"selectedFontIds": [
|
||||
"0001",
|
||||
"0003",
|
||||
"0006",
|
||||
"0011",
|
||||
"0015"
|
||||
],
|
||||
"favoriteFontIds": [
|
||||
"0001",
|
||||
"0003",
|
||||
"0006",
|
||||
"0011",
|
||||
"0015"
|
||||
]
|
||||
}
|
||||
@@ -1,167 +1,167 @@
|
||||
module.exports = [
|
||||
{
|
||||
"id": "其他字体/AlimamaDaoLiTi",
|
||||
"id": "0001",
|
||||
"name": "AlimamaDaoLiTi",
|
||||
"filename": "AlimamaDaoLiTi.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/Hangeuljaemin4-Regular",
|
||||
"id": "0002",
|
||||
"name": "Hangeuljaemin4-Regular",
|
||||
"filename": "Hangeuljaemin4-Regular.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/I.顏體",
|
||||
"id": "0003",
|
||||
"name": "I.顏體",
|
||||
"filename": "I.顏體.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/I.顏體.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/XCDUANZHUANGSONGTI",
|
||||
"id": "0004",
|
||||
"name": "XCDUANZHUANGSONGTI",
|
||||
"filename": "XCDUANZHUANGSONGTI.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/qiji-combo",
|
||||
"id": "0005",
|
||||
"name": "qiji-combo",
|
||||
"filename": "qiji-combo.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/qiji-combo.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/临海隶书",
|
||||
"id": "0006",
|
||||
"name": "临海隶书",
|
||||
"filename": "临海隶书.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/临海隶书.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/京華老宋体_KingHwa_OldSong",
|
||||
"id": "0007",
|
||||
"name": "京華老宋体_KingHwa_OldSong",
|
||||
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/优设标题黑",
|
||||
"id": "0008",
|
||||
"name": "优设标题黑",
|
||||
"filename": "优设标题黑.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/优设标题黑.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/包图小白体",
|
||||
"id": "0009",
|
||||
"name": "包图小白体",
|
||||
"filename": "包图小白体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/包图小白体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/源界明朝",
|
||||
"id": "0010",
|
||||
"name": "源界明朝",
|
||||
"filename": "源界明朝.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/源界明朝.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/演示佛系体",
|
||||
"id": "0011",
|
||||
"name": "演示佛系体",
|
||||
"filename": "演示佛系体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/演示佛系体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/站酷快乐体",
|
||||
"id": "0012",
|
||||
"name": "站酷快乐体",
|
||||
"filename": "站酷快乐体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/问藏书房",
|
||||
"id": "0013",
|
||||
"name": "问藏书房",
|
||||
"filename": "问藏书房.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/问藏书房.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/霞鹜臻楷",
|
||||
"id": "0014",
|
||||
"name": "霞鹜臻楷",
|
||||
"filename": "霞鹜臻楷.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
||||
},
|
||||
{
|
||||
"id": "庞门正道/庞门正道标题体",
|
||||
"id": "0015",
|
||||
"name": "庞门正道标题体",
|
||||
"filename": "庞门正道标题体.ttf",
|
||||
"category": "庞门正道",
|
||||
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "庞门正道-测试/庞门正道标题体",
|
||||
"id": "0016",
|
||||
"name": "庞门正道标题体",
|
||||
"filename": "庞门正道标题体.ttf",
|
||||
"category": "庞门正道-测试",
|
||||
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗勘亭流繁",
|
||||
"id": "0017",
|
||||
"name": "王漢宗勘亭流繁",
|
||||
"filename": "王漢宗勘亭流繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗新潮體",
|
||||
"id": "0018",
|
||||
"name": "王漢宗新潮體",
|
||||
"filename": "王漢宗新潮體.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗波卡體空陰",
|
||||
"id": "0019",
|
||||
"name": "王漢宗波卡體空陰",
|
||||
"filename": "王漢宗波卡體空陰.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗細黑體繁",
|
||||
"id": "0020",
|
||||
"name": "王漢宗細黑體繁",
|
||||
"filename": "王漢宗細黑體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗綜藝體雙空陰",
|
||||
"id": "0021",
|
||||
"name": "王漢宗綜藝體雙空陰",
|
||||
"filename": "王漢宗綜藝體雙空陰.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗超明體繁",
|
||||
"id": "0022",
|
||||
"name": "王漢宗超明體繁",
|
||||
"filename": "王漢宗超明體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗酷儷海報",
|
||||
"id": "0023",
|
||||
"name": "王漢宗酷儷海報",
|
||||
"filename": "王漢宗酷儷海報.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗顏楷體繁",
|
||||
"id": "0024",
|
||||
"name": "王漢宗顏楷體繁",
|
||||
"filename": "王漢宗顏楷體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
|
||||
@@ -1,167 +1,167 @@
|
||||
[
|
||||
{
|
||||
"id": "其他字体/AlimamaDaoLiTi",
|
||||
"id": "0001",
|
||||
"name": "AlimamaDaoLiTi",
|
||||
"filename": "AlimamaDaoLiTi.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/Hangeuljaemin4-Regular",
|
||||
"id": "0002",
|
||||
"name": "Hangeuljaemin4-Regular",
|
||||
"filename": "Hangeuljaemin4-Regular.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/I.顏體",
|
||||
"id": "0003",
|
||||
"name": "I.顏體",
|
||||
"filename": "I.顏體.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/I.顏體.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/XCDUANZHUANGSONGTI",
|
||||
"id": "0004",
|
||||
"name": "XCDUANZHUANGSONGTI",
|
||||
"filename": "XCDUANZHUANGSONGTI.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/qiji-combo",
|
||||
"id": "0005",
|
||||
"name": "qiji-combo",
|
||||
"filename": "qiji-combo.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/qiji-combo.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/临海隶书",
|
||||
"id": "0006",
|
||||
"name": "临海隶书",
|
||||
"filename": "临海隶书.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/临海隶书.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/京華老宋体_KingHwa_OldSong",
|
||||
"id": "0007",
|
||||
"name": "京華老宋体_KingHwa_OldSong",
|
||||
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/优设标题黑",
|
||||
"id": "0008",
|
||||
"name": "优设标题黑",
|
||||
"filename": "优设标题黑.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/优设标题黑.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/包图小白体",
|
||||
"id": "0009",
|
||||
"name": "包图小白体",
|
||||
"filename": "包图小白体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/包图小白体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/源界明朝",
|
||||
"id": "0010",
|
||||
"name": "源界明朝",
|
||||
"filename": "源界明朝.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/源界明朝.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/演示佛系体",
|
||||
"id": "0011",
|
||||
"name": "演示佛系体",
|
||||
"filename": "演示佛系体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/演示佛系体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/站酷快乐体",
|
||||
"id": "0012",
|
||||
"name": "站酷快乐体",
|
||||
"filename": "站酷快乐体.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/问藏书房",
|
||||
"id": "0013",
|
||||
"name": "问藏书房",
|
||||
"filename": "问藏书房.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/问藏书房.ttf"
|
||||
},
|
||||
{
|
||||
"id": "其他字体/霞鹜臻楷",
|
||||
"id": "0014",
|
||||
"name": "霞鹜臻楷",
|
||||
"filename": "霞鹜臻楷.ttf",
|
||||
"category": "其他字体",
|
||||
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
||||
},
|
||||
{
|
||||
"id": "庞门正道/庞门正道标题体",
|
||||
"id": "0015",
|
||||
"name": "庞门正道标题体",
|
||||
"filename": "庞门正道标题体.ttf",
|
||||
"category": "庞门正道",
|
||||
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "庞门正道-测试/庞门正道标题体",
|
||||
"id": "0016",
|
||||
"name": "庞门正道标题体",
|
||||
"filename": "庞门正道标题体.ttf",
|
||||
"category": "庞门正道-测试",
|
||||
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗勘亭流繁",
|
||||
"id": "0017",
|
||||
"name": "王漢宗勘亭流繁",
|
||||
"filename": "王漢宗勘亭流繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗新潮體",
|
||||
"id": "0018",
|
||||
"name": "王漢宗新潮體",
|
||||
"filename": "王漢宗新潮體.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗波卡體空陰",
|
||||
"id": "0019",
|
||||
"name": "王漢宗波卡體空陰",
|
||||
"filename": "王漢宗波卡體空陰.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗細黑體繁",
|
||||
"id": "0020",
|
||||
"name": "王漢宗細黑體繁",
|
||||
"filename": "王漢宗細黑體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗綜藝體雙空陰",
|
||||
"id": "0021",
|
||||
"name": "王漢宗綜藝體雙空陰",
|
||||
"filename": "王漢宗綜藝體雙空陰.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗超明體繁",
|
||||
"id": "0022",
|
||||
"name": "王漢宗超明體繁",
|
||||
"filename": "王漢宗超明體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗酷儷海報",
|
||||
"id": "0023",
|
||||
"name": "王漢宗酷儷海報",
|
||||
"filename": "王漢宗酷儷海報.ttf",
|
||||
"category": "王漢宗",
|
||||
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
||||
},
|
||||
{
|
||||
"id": "王漢宗/王漢宗顏楷體繁",
|
||||
"id": "0024",
|
||||
"name": "王漢宗顏楷體繁",
|
||||
"filename": "王漢宗顏楷體繁.ttf",
|
||||
"category": "王漢宗",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// CDN 配置文件
|
||||
// 管理所有静态资源的 CDN 地址
|
||||
|
||||
const CDN_BASE_URL = 'https://fonts.biboer.cn';
|
||||
const { buildRuntimeConfig } = require('./server')
|
||||
const runtimeConfig = buildRuntimeConfig()
|
||||
const CDN_BASE_URL = runtimeConfig.fontsBaseUrl;
|
||||
|
||||
// 图标路径配置
|
||||
const ICON_PATHS = {
|
||||
@@ -30,7 +32,7 @@ const ICON_PATHS = {
|
||||
|
||||
// 字体资源路径
|
||||
const FONT_BASE_URL = `${CDN_BASE_URL}/fonts`;
|
||||
const FONTS_JSON_URL = `${CDN_BASE_URL}/fonts.json`;
|
||||
const FONTS_JSON_URL = runtimeConfig.fontsManifestUrl;
|
||||
|
||||
module.exports = {
|
||||
CDN_BASE_URL,
|
||||
|
||||
54
miniprogram/config/server.js
Normal file
54
miniprogram/config/server.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// 远端服务配置(统一修改入口)
|
||||
// 更换服务器时,仅需修改这里。
|
||||
|
||||
const SERVER_CONFIG = {
|
||||
protocol: 'https',
|
||||
host: 'fonts.biboer.cn',
|
||||
// 留空表示使用协议默认端口(https:443 / http:80)
|
||||
port: '',
|
||||
apiPrefix: '/api',
|
||||
fontsManifestPath: '/miniprogram/assets/fonts.json',
|
||||
defaultConfigPath: '/miniprogram/assets/default.json',
|
||||
}
|
||||
|
||||
function buildOrigin() {
|
||||
const protocol = String(SERVER_CONFIG.protocol || 'https').replace(/:$/, '')
|
||||
const host = String(SERVER_CONFIG.host || '').trim()
|
||||
const port = String(SERVER_CONFIG.port || '').trim()
|
||||
|
||||
if (!host) {
|
||||
throw new Error('SERVER_CONFIG.host 未配置')
|
||||
}
|
||||
|
||||
const hasDefaultPort = (protocol === 'https' && port === '443') || (protocol === 'http' && port === '80')
|
||||
const portPart = !port || hasDefaultPort ? '' : `:${port}`
|
||||
return `${protocol}://${host}${portPart}`
|
||||
}
|
||||
|
||||
function normalizePath(path, fallback) {
|
||||
const value = String(path || fallback || '').trim()
|
||||
if (!value) {
|
||||
return '/'
|
||||
}
|
||||
return value.startsWith('/') ? value : `/${value}`
|
||||
}
|
||||
|
||||
function buildRuntimeConfig() {
|
||||
const origin = buildOrigin()
|
||||
const apiPrefix = normalizePath(SERVER_CONFIG.apiPrefix, '/api').replace(/\/$/, '')
|
||||
const fontsManifestPath = normalizePath(SERVER_CONFIG.fontsManifestPath, '/miniprogram/assets/fonts.json')
|
||||
const defaultConfigPath = normalizePath(SERVER_CONFIG.defaultConfigPath, '/miniprogram/assets/default.json')
|
||||
|
||||
return {
|
||||
fontsBaseUrl: origin,
|
||||
fontsManifestUrl: `${origin}${fontsManifestPath}`,
|
||||
defaultConfigUrl: `${origin}${defaultConfigPath}`,
|
||||
svgRenderApiUrl: `${origin}${apiPrefix}/render-svg`,
|
||||
pngRenderApiUrl: `${origin}${apiPrefix}/render-png`,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SERVER_CONFIG,
|
||||
buildRuntimeConfig,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const { loadFontsManifest } = require('../../utils/mp/font-loader')
|
||||
const { loadFontsManifest, loadDefaultConfig } = require('../../utils/mp/font-loader')
|
||||
const { loadAppState, saveAppState, loadFavorites, saveFavorites } = require('../../utils/mp/storage')
|
||||
const {
|
||||
shareSvgFromUserTap,
|
||||
@@ -11,8 +11,11 @@ const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#
|
||||
const FONT_SIZE_MIN = 20
|
||||
const FONT_SIZE_MAX = 120
|
||||
const PREVIEW_RENDER_FONT_SIZE = 120
|
||||
const PREVIEW_MAX_CHARS_PER_LINE = 45
|
||||
const PREVIEW_CACHE_LIMIT = 300
|
||||
const MIN_PREVIEW_IMAGE_WIDTH = 24
|
||||
const MAX_PREVIEW_IMAGE_WIDTH = 2400
|
||||
const FEEDBACK_EMAIL = 'douboer@gmail.com'
|
||||
|
||||
// 临时使用本地图标 - 根据Figma annotation配置
|
||||
const LOCAL_ICON_PATHS = {
|
||||
@@ -58,6 +61,31 @@ function clampFontSize(value, fallback = PREVIEW_RENDER_FONT_SIZE) {
|
||||
return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(base)))
|
||||
}
|
||||
|
||||
function normalizeSelectedFontIds(fontIds) {
|
||||
if (!Array.isArray(fontIds)) {
|
||||
return []
|
||||
}
|
||||
return Array.from(
|
||||
new Set(
|
||||
fontIds
|
||||
.map((id) => String(id || '').trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function buildLegacyFontId(font) {
|
||||
if (!font || typeof font !== 'object') {
|
||||
return ''
|
||||
}
|
||||
const category = String(font.category || '').trim()
|
||||
const name = String(font.name || '').trim()
|
||||
if (!category || !name) {
|
||||
return ''
|
||||
}
|
||||
return `${category}/${name}`
|
||||
}
|
||||
|
||||
function extractErrorMessage(error, fallback) {
|
||||
if (!error) {
|
||||
return fallback
|
||||
@@ -84,6 +112,25 @@ function writePngBufferToTempFile(pngBuffer, fontName) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
function buildPreviewCacheKey(fontId, text, letterSpacing, maxCharsPerLine = PREVIEW_MAX_CHARS_PER_LINE) {
|
||||
const safeFontId = String(fontId || '')
|
||||
const safeText = String(text || '')
|
||||
const spacingNumber = Number(letterSpacing)
|
||||
const safeSpacing = Number.isFinite(spacingNumber) ? spacingNumber.toFixed(4) : '0.0000'
|
||||
return [safeFontId, safeSpacing, String(maxCharsPerLine), safeText].join('::')
|
||||
}
|
||||
|
||||
function setPreviewCache(previewCache, key, value) {
|
||||
previewCache.set(key, value)
|
||||
if (previewCache.size <= PREVIEW_CACHE_LIMIT) {
|
||||
return
|
||||
}
|
||||
const oldestKey = previewCache.keys().next().value
|
||||
if (oldestKey !== undefined) {
|
||||
previewCache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
function formatSvgNumber(value) {
|
||||
const text = String(Number(value).toFixed(2))
|
||||
return text.replace(/\.?0+$/, '')
|
||||
@@ -146,9 +193,9 @@ function applyLocalStyleToFontItem(font, fontSize, color) {
|
||||
Page({
|
||||
data: {
|
||||
inputText: '星程字体转换',
|
||||
fontSize: FONT_SIZE_MAX,
|
||||
fontSize: 50,
|
||||
letterSpacingInput: '0',
|
||||
textColor: '#000000',
|
||||
textColor: '#dc2626',
|
||||
colorPalette: COLOR_PALETTE,
|
||||
selectedFonts: [], // 当前已选中的字体列表
|
||||
fontCategories: [], // 字体分类树
|
||||
@@ -160,6 +207,7 @@ Page({
|
||||
// 搜索功能
|
||||
searchKeyword: '',
|
||||
showSearch: true,
|
||||
feedbackEmail: FEEDBACK_EMAIL,
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
@@ -170,6 +218,8 @@ Page({
|
||||
console.log('============================')
|
||||
|
||||
this.fontMap = new Map()
|
||||
this.legacyFontIdMap = new Map()
|
||||
this.previewCache = new Map()
|
||||
this.generateTimer = null
|
||||
this.categoryExpandedMap = {}
|
||||
|
||||
@@ -177,7 +227,11 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const favorites = loadFavorites()
|
||||
const rawFavorites = loadFavorites()
|
||||
const favorites = this.normalizeFontIdList(rawFavorites)
|
||||
if (normalizeSelectedFontIds(rawFavorites).join(',') !== favorites.join(',')) {
|
||||
saveFavorites(favorites)
|
||||
}
|
||||
this.setData({ favorites })
|
||||
this.updateFontTrees()
|
||||
},
|
||||
@@ -196,35 +250,126 @@ Page({
|
||||
this.setData({ selectedFonts })
|
||||
},
|
||||
|
||||
buildFontMaps(fonts) {
|
||||
this.fontMap = new Map()
|
||||
this.legacyFontIdMap = new Map()
|
||||
|
||||
if (!Array.isArray(fonts)) {
|
||||
return
|
||||
}
|
||||
|
||||
fonts.forEach((font) => {
|
||||
const fontId = String(font.id || '').trim()
|
||||
if (!fontId) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fontMap.set(fontId, font)
|
||||
|
||||
const legacyId = buildLegacyFontId(font)
|
||||
if (legacyId && !this.legacyFontIdMap.has(legacyId)) {
|
||||
this.legacyFontIdMap.set(legacyId, fontId)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resolveFontId(fontId) {
|
||||
const normalizedId = String(fontId || '').trim()
|
||||
if (!normalizedId) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const hasFontMap = this.fontMap instanceof Map && this.fontMap.size > 0
|
||||
if (!hasFontMap) {
|
||||
return normalizedId
|
||||
}
|
||||
|
||||
if (this.fontMap.has(normalizedId)) {
|
||||
return normalizedId
|
||||
}
|
||||
|
||||
if (this.legacyFontIdMap instanceof Map && this.legacyFontIdMap.has(normalizedId)) {
|
||||
return this.legacyFontIdMap.get(normalizedId) || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
|
||||
normalizeFontIdList(fontIds) {
|
||||
const normalized = normalizeSelectedFontIds(fontIds)
|
||||
return Array.from(
|
||||
new Set(
|
||||
normalized
|
||||
.map((fontId) => this.resolveFontId(fontId))
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
async bootstrap() {
|
||||
wx.showLoading({ title: '加载中', mask: true })
|
||||
try {
|
||||
const state = loadAppState()
|
||||
const fonts = await loadFontsManifest()
|
||||
const favorites = loadFavorites()
|
||||
const isFirstLaunch = !state || !state.updatedAt
|
||||
const [fonts, defaultConfig] = await Promise.all([
|
||||
loadFontsManifest(),
|
||||
loadDefaultConfig(),
|
||||
])
|
||||
this.buildFontMaps(fonts)
|
||||
|
||||
for (const font of fonts) {
|
||||
this.fontMap.set(font.id, font)
|
||||
const rawFavorites = loadFavorites()
|
||||
const normalizedStoredFavorites = this.normalizeFontIdList(rawFavorites)
|
||||
const normalizedDefaultFavorites = this.normalizeFontIdList(defaultConfig.favoriteFontIds)
|
||||
const favorites = isFirstLaunch ? normalizedDefaultFavorites : normalizedStoredFavorites
|
||||
|
||||
if (
|
||||
normalizeSelectedFontIds(rawFavorites).join(',') !== favorites.join(',') ||
|
||||
(isFirstLaunch && favorites.length > 0)
|
||||
) {
|
||||
saveFavorites(favorites)
|
||||
}
|
||||
|
||||
const initialInputText = isFirstLaunch
|
||||
? ((typeof defaultConfig.inputText === 'string' && defaultConfig.inputText) || this.data.inputText)
|
||||
: (state.inputText || this.data.inputText)
|
||||
const initialFontSize = isFirstLaunch
|
||||
? clampFontSize(defaultConfig.fontSize, this.data.fontSize)
|
||||
: clampFontSize(state.fontSize, this.data.fontSize)
|
||||
const initialLetterSpacingInput = isFirstLaunch
|
||||
? (
|
||||
typeof defaultConfig.letterSpacing === 'number'
|
||||
? String(defaultConfig.letterSpacing)
|
||||
: this.data.letterSpacingInput
|
||||
)
|
||||
: (
|
||||
typeof state.letterSpacing === 'number'
|
||||
? String(state.letterSpacing)
|
||||
: this.data.letterSpacingInput
|
||||
)
|
||||
const initialTextColor = isFirstLaunch
|
||||
? normalizeHexColor(defaultConfig.textColor || this.data.textColor)
|
||||
: normalizeHexColor(state.textColor || this.data.textColor)
|
||||
|
||||
this.setData({
|
||||
inputText: state.inputText || this.data.inputText,
|
||||
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),
|
||||
inputText: initialInputText,
|
||||
fontSize: initialFontSize,
|
||||
letterSpacingInput: initialLetterSpacingInput,
|
||||
textColor: initialTextColor,
|
||||
favorites,
|
||||
})
|
||||
this.categoryExpandedMap = state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
|
||||
this.categoryExpandedMap = !isFirstLaunch && state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
|
||||
? { ...state.categoryExpandedMap }
|
||||
: {}
|
||||
|
||||
// 构建字体树
|
||||
this.updateFontTrees()
|
||||
|
||||
// 如果有保存的选中字体,恢复它们
|
||||
if (state.selectedFontIds && state.selectedFontIds.length > 0) {
|
||||
const selectedFonts = state.selectedFontIds
|
||||
// 恢复选中字体(首次使用走 default.json,后续走本地用户配置)
|
||||
const rawSelectedFontIds = isFirstLaunch ? defaultConfig.selectedFontIds : state.selectedFontIds
|
||||
const normalizedStoredSelectedIds = normalizeSelectedFontIds(rawSelectedFontIds)
|
||||
const initialSelectedFontIds = this.normalizeFontIdList(normalizedStoredSelectedIds)
|
||||
if (initialSelectedFontIds.length > 0) {
|
||||
const selectedFonts = initialSelectedFontIds
|
||||
.map(id => this.fontMap.get(id))
|
||||
.filter(font => font)
|
||||
.map(font => ({
|
||||
@@ -240,6 +385,21 @@ Page({
|
||||
this.updateFontTrees()
|
||||
await this.generateAllPreviews()
|
||||
}
|
||||
|
||||
const migratedSelectedIdsChanged = !isFirstLaunch &&
|
||||
normalizedStoredSelectedIds.join(',') !== initialSelectedFontIds.join(',')
|
||||
|
||||
// 首次加载后立即固化配置,后续全部以用户配置为准
|
||||
if (isFirstLaunch || migratedSelectedIdsChanged) {
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
selectedFontIds: this.data.selectedFonts.map(font => font.id),
|
||||
fontSize: Number(this.data.fontSize),
|
||||
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||
textColor: this.data.textColor,
|
||||
categoryExpandedMap: this.categoryExpandedMap,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : '初始化失败'
|
||||
wx.showToast({ title: message, icon: 'none', duration: 2200 })
|
||||
@@ -317,7 +477,7 @@ Page({
|
||||
cat.allSelected = false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
favoriteFonts.sort((a, b) => {
|
||||
const categoryCompare = String(a.category || '').localeCompare(String(b.category || ''))
|
||||
if (categoryCompare !== 0) return categoryCompare
|
||||
@@ -430,7 +590,7 @@ Page({
|
||||
},
|
||||
|
||||
// 切换分类全选/取消全选
|
||||
onToggleSelectAllInCategory(e) {
|
||||
async onToggleSelectAllInCategory(e) {
|
||||
const category = e.currentTarget.dataset.category
|
||||
if (!category) return
|
||||
|
||||
@@ -438,8 +598,9 @@ Page({
|
||||
if (!categoryFonts || categoryFonts.fonts.length === 0) return
|
||||
|
||||
const allSelected = categoryFonts.allSelected
|
||||
const selectedFonts = [...this.data.selectedFonts]
|
||||
const selectedIdSet = new Set(selectedFonts.map(f => f.id))
|
||||
const previousSelectedMap = new Map(this.data.selectedFonts.map(font => [font.id, font]))
|
||||
const selectedIdSet = new Set(previousSelectedMap.keys())
|
||||
const addedFontIds = []
|
||||
|
||||
if (allSelected) {
|
||||
// 取消全选:移除该分类下的所有字体
|
||||
@@ -449,6 +610,9 @@ Page({
|
||||
} else {
|
||||
// 全选:添加该分类下的所有字体
|
||||
categoryFonts.fonts.forEach(font => {
|
||||
if (!selectedIdSet.has(font.id)) {
|
||||
addedFontIds.push(font.id)
|
||||
}
|
||||
selectedIdSet.add(font.id)
|
||||
})
|
||||
}
|
||||
@@ -456,19 +620,32 @@ Page({
|
||||
const newSelectedFonts = []
|
||||
this.fontMap.forEach(font => {
|
||||
if (selectedIdSet.has(font.id)) {
|
||||
newSelectedFonts.push({
|
||||
id: font.id,
|
||||
name: font.name,
|
||||
category: font.category,
|
||||
showInPreview: true,
|
||||
previewSrc: '',
|
||||
})
|
||||
const existingFont = previousSelectedMap.get(font.id)
|
||||
if (existingFont) {
|
||||
newSelectedFonts.push({
|
||||
...existingFont,
|
||||
showInPreview: typeof existingFont.showInPreview === 'boolean' ? existingFont.showInPreview : true,
|
||||
})
|
||||
} else {
|
||||
newSelectedFonts.push({
|
||||
id: font.id,
|
||||
name: font.name,
|
||||
category: font.category,
|
||||
showInPreview: true,
|
||||
previewSrc: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.setData({ selectedFonts: newSelectedFonts })
|
||||
this.updateFontTrees()
|
||||
this.scheduleGenerate()
|
||||
|
||||
if (addedFontIds.length > 0) {
|
||||
for (const fontId of addedFontIds) {
|
||||
await this.generatePreviewForFont(fontId)
|
||||
}
|
||||
}
|
||||
|
||||
saveAppState({
|
||||
inputText: this.data.inputText,
|
||||
@@ -488,6 +665,26 @@ Page({
|
||||
|
||||
try {
|
||||
const letterSpacing = Number(this.data.letterSpacingInput || 0)
|
||||
const cacheKey = buildPreviewCacheKey(fontId, text, letterSpacing, PREVIEW_MAX_CHARS_PER_LINE)
|
||||
const cached = this.previewCache.get(cacheKey)
|
||||
|
||||
if (cached) {
|
||||
const selectedFonts = this.data.selectedFonts
|
||||
const index = selectedFonts.findIndex(f => f.id === fontId)
|
||||
if (index >= 0) {
|
||||
selectedFonts[index].baseSvg = cached.svg
|
||||
selectedFonts[index].baseWidth = cached.width
|
||||
selectedFonts[index].baseHeight = cached.height
|
||||
selectedFonts[index].renderFontSize = PREVIEW_RENDER_FONT_SIZE
|
||||
selectedFonts[index] = applyLocalStyleToFontItem(
|
||||
selectedFonts[index],
|
||||
Number(this.data.fontSize),
|
||||
this.data.textColor,
|
||||
)
|
||||
this.setData({ selectedFonts })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const result = await renderSvgByApi({
|
||||
fontId,
|
||||
@@ -496,7 +693,12 @@ Page({
|
||||
fontSize: PREVIEW_RENDER_FONT_SIZE,
|
||||
fillColor: '#000000',
|
||||
letterSpacing,
|
||||
maxCharsPerLine: 45,
|
||||
maxCharsPerLine: PREVIEW_MAX_CHARS_PER_LINE,
|
||||
})
|
||||
setPreviewCache(this.previewCache, cacheKey, {
|
||||
svg: result.svg,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
})
|
||||
|
||||
// 更新对应字体的预览
|
||||
@@ -850,4 +1052,42 @@ Page({
|
||||
this.setData({ searchKeyword: keyword })
|
||||
this.updateFontTrees()
|
||||
},
|
||||
|
||||
onTapFeedbackEmail() {
|
||||
const email = this.data.feedbackEmail || FEEDBACK_EMAIL
|
||||
const mailtoUrl = `mailto:${email}`
|
||||
|
||||
// 微信环境对 mailto 支持不稳定:优先尝试跳转,失败时自动复制邮箱
|
||||
if (typeof wx.openUrl === 'function') {
|
||||
wx.openUrl({
|
||||
url: mailtoUrl,
|
||||
fail: () => {
|
||||
this.copyFeedbackEmail(email)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.copyFeedbackEmail(email)
|
||||
},
|
||||
|
||||
copyFeedbackEmail(email) {
|
||||
wx.setClipboardData({
|
||||
data: String(email || FEEDBACK_EMAIL),
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '已复制邮箱',
|
||||
content: '请打开邮件应用并粘贴收件人地址发送反馈。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '邮箱复制失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
|
||||
<!-- 版权说明 -->
|
||||
<view class="copyright-footer">
|
||||
@版权说明:仅SVG和PNG分享,无TTF下载,如侵权,反馈:douboer@gmail.com
|
||||
<text>@版权说明:仅SVG和PNG分享,无TTF下载,如侵权,反馈:</text>
|
||||
<text class="copyright-email-link" bindtap="onTapFeedbackEmail">{{feedbackEmail}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 颜色选择器弹窗 -->
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
.preview-content {
|
||||
background: transparent;
|
||||
padding: 4rpx 0;
|
||||
min-height: 80rpx;
|
||||
min-height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -284,6 +284,11 @@
|
||||
padding: 8rpx 8rpx;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.favorite-selection {
|
||||
border-left: 2rpx solid #3EE4C3;
|
||||
}
|
||||
|
||||
/* 搜索相关样式 */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { request, downloadFile, readFile } = require('./wx-promisify')
|
||||
|
||||
const localFonts = require('../../assets/fonts')
|
||||
const localDefaultConfig = require('../../assets/default')
|
||||
|
||||
const fontBufferCache = new Map()
|
||||
const MAX_FONT_CACHE = 4
|
||||
@@ -44,6 +45,70 @@ function normalizeManifest(fonts, baseUrl) {
|
||||
.filter((item) => item.url)
|
||||
}
|
||||
|
||||
function normalizeDefaultConfig(config) {
|
||||
const payload = config && typeof config === 'object' ? config : {}
|
||||
const selectedRaw = Array.isArray(payload.selectedFontIds)
|
||||
? payload.selectedFontIds
|
||||
: (Array.isArray(payload.selectedFonts) ? payload.selectedFonts : [])
|
||||
const favoriteRaw = Array.isArray(payload.favoriteFontIds)
|
||||
? payload.favoriteFontIds
|
||||
: (Array.isArray(payload.favoriteFonts) ? payload.favoriteFonts : [])
|
||||
|
||||
const selectedFontIds = Array.from(
|
||||
new Set(
|
||||
selectedRaw
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
const favoriteFontIds = Array.from(
|
||||
new Set(
|
||||
favoriteRaw
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
)
|
||||
|
||||
const result = {
|
||||
selectedFontIds,
|
||||
favoriteFontIds,
|
||||
}
|
||||
|
||||
if (typeof payload.inputText === 'string') {
|
||||
result.inputText = payload.inputText
|
||||
}
|
||||
if (typeof payload.textColor === 'string') {
|
||||
result.textColor = payload.textColor
|
||||
}
|
||||
if (payload.fontSize !== undefined && payload.fontSize !== null && payload.fontSize !== '') {
|
||||
const fontSize = Number(payload.fontSize)
|
||||
if (Number.isFinite(fontSize)) {
|
||||
result.fontSize = fontSize
|
||||
}
|
||||
}
|
||||
if (payload.letterSpacing !== undefined && payload.letterSpacing !== null && payload.letterSpacing !== '') {
|
||||
const letterSpacing = Number(payload.letterSpacing)
|
||||
if (Number.isFinite(letterSpacing)) {
|
||||
result.letterSpacing = letterSpacing
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function buildDefaultConfigUrl(manifestUrl, baseUrl) {
|
||||
const manifest = String(manifestUrl || '').trim()
|
||||
if (manifest) {
|
||||
const withoutHash = manifest.split('#')[0]
|
||||
const [pathPart] = withoutHash.split('?')
|
||||
const slashIndex = pathPart.lastIndexOf('/')
|
||||
if (slashIndex >= 0) {
|
||||
return `${pathPart.slice(0, slashIndex + 1)}default.json`
|
||||
}
|
||||
}
|
||||
return normalizePath('/miniprogram/assets/default.json', baseUrl)
|
||||
}
|
||||
|
||||
async function loadFontsManifest(options = {}) {
|
||||
const app = getApp()
|
||||
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
|
||||
@@ -79,6 +144,40 @@ async function loadFontsManifest(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDefaultConfig(options = {}) {
|
||||
const app = getApp()
|
||||
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
|
||||
const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl
|
||||
const defaultConfigUrl = options.defaultConfigUrl ||
|
||||
app.globalData.defaultConfigUrl ||
|
||||
buildDefaultConfigUrl(manifestUrl, baseUrl)
|
||||
|
||||
if (app.globalData.defaultConfig && typeof app.globalData.defaultConfig === 'object') {
|
||||
return app.globalData.defaultConfig
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request({
|
||||
url: defaultConfigUrl,
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw new Error(`获取默认配置失败,状态码: ${response.statusCode}`)
|
||||
}
|
||||
|
||||
const remoteConfig = normalizeDefaultConfig(response.data)
|
||||
app.globalData.defaultConfig = remoteConfig
|
||||
return remoteConfig
|
||||
} catch (error) {
|
||||
console.warn('远程 default.json 加载失败,回退到本地默认配置:', error)
|
||||
const fallbackConfig = normalizeDefaultConfig(localDefaultConfig)
|
||||
app.globalData.defaultConfig = fallbackConfig
|
||||
return fallbackConfig
|
||||
}
|
||||
}
|
||||
|
||||
function setLruCache(key, value) {
|
||||
if (fontBufferCache.has(key)) {
|
||||
fontBufferCache.delete(key)
|
||||
@@ -125,6 +224,7 @@ function listCategories(fonts) {
|
||||
|
||||
module.exports = {
|
||||
loadFontsManifest,
|
||||
loadDefaultConfig,
|
||||
loadFontBuffer,
|
||||
listCategories,
|
||||
}
|
||||
|
||||
@@ -86,9 +86,12 @@ async function renderPngByApi(payload) {
|
||||
? Number(app.globalData.apiTimeoutMs)
|
||||
: 30000
|
||||
const baseApiUrl = buildApiUrl()
|
||||
const apiUrl = /\/api\/render-svg$/.test(baseApiUrl)
|
||||
? baseApiUrl.replace(/\/api\/render-svg$/, '/api/render-png')
|
||||
: `${baseApiUrl.replace(/\/$/, '')}/render-png`
|
||||
const configuredPngApiUrl = app && app.globalData ? app.globalData.pngRenderApiUrl : ''
|
||||
const apiUrl = configuredPngApiUrl || (
|
||||
/\/api\/render-svg$/.test(baseApiUrl)
|
||||
? baseApiUrl.replace(/\/api\/render-svg$/, '/api/render-png')
|
||||
: `${baseApiUrl.replace(/\/$/, '')}/render-png`
|
||||
)
|
||||
|
||||
const response = await request({
|
||||
url: apiUrl,
|
||||
|
||||
@@ -36,7 +36,14 @@ function run() {
|
||||
const wrapped = wrapTextByChars('123456', 2)
|
||||
assert(wrapped === '12\n34\n56', 'wrapTextByChars 结果不符合预期')
|
||||
|
||||
const fontFile = findFirstFontFile(path.join(__dirname, '..', 'frontend', 'public', 'fonts'))
|
||||
const fontRootCandidates = [
|
||||
path.join(__dirname, '..', 'fonts'),
|
||||
path.join(__dirname, '..', 'frontend', 'public', 'fonts'),
|
||||
]
|
||||
const fontRoot = fontRootCandidates.find((dir) => fs.existsSync(dir))
|
||||
assert(fontRoot, '未找到字体目录(fonts 或 frontend/public/fonts)')
|
||||
|
||||
const fontFile = findFirstFontFile(fontRoot)
|
||||
assert(fontFile, '未找到可用字体文件')
|
||||
|
||||
const buffer = fs.readFileSync(fontFile)
|
||||
|
||||
@@ -8,8 +8,12 @@ set -e # 遇到错误立即退出
|
||||
# ===== 配置区域 =====
|
||||
SERVER="user@fonts.biboer.cn" # 请替换为你的 SSH 用户名
|
||||
REMOTE_DIR="/home/gavin/font2svg"
|
||||
LOCAL_FONTS_DIR="frontend/public/fonts"
|
||||
LOCAL_FONTS_JSON="frontend/public/fonts.json"
|
||||
REMOTE_MP_ASSETS_DIR="$REMOTE_DIR/miniprogram/assets"
|
||||
LOCAL_FONTS_DIR="fonts"
|
||||
LOCAL_WEB_FONTS_JSON="frontend/public/fonts.json"
|
||||
LOCAL_WEB_DEFAULT_JSON="frontend/public/default.json"
|
||||
LOCAL_MP_FONTS_JSON="miniprogram/assets/fonts.json"
|
||||
LOCAL_MP_DEFAULT_JSON="miniprogram/assets/default.json"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
@@ -38,8 +42,13 @@ check_files() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOCAL_FONTS_JSON" ]; then
|
||||
log_error "fonts.json 文件不存在: $LOCAL_FONTS_JSON"
|
||||
if [ ! -f "$LOCAL_WEB_FONTS_JSON" ]; then
|
||||
log_error "Web fonts.json 文件不存在: $LOCAL_WEB_FONTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOCAL_MP_FONTS_JSON" ]; then
|
||||
log_error "小程序 fonts.json 文件不存在: $LOCAL_MP_FONTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -49,7 +58,7 @@ check_files() {
|
||||
|
||||
create_remote_dirs() {
|
||||
log_info "创建远程目录..."
|
||||
ssh $SERVER "mkdir -p $REMOTE_DIR/fonts"
|
||||
ssh $SERVER "mkdir -p $REMOTE_DIR/fonts $REMOTE_MP_ASSETS_DIR"
|
||||
}
|
||||
|
||||
upload_fonts() {
|
||||
@@ -65,34 +74,55 @@ upload_fonts() {
|
||||
log_info "字体文件上传完成"
|
||||
}
|
||||
|
||||
upload_fonts_json() {
|
||||
log_info "上传 fonts.json..."
|
||||
scp "$LOCAL_FONTS_JSON" "$SERVER:$REMOTE_DIR/"
|
||||
log_info "fonts.json 上传完成"
|
||||
upload_web_config() {
|
||||
log_info "上传 Web 配置..."
|
||||
scp "$LOCAL_WEB_FONTS_JSON" "$SERVER:$REMOTE_DIR/fonts.json"
|
||||
if [ -f "$LOCAL_WEB_DEFAULT_JSON" ]; then
|
||||
scp "$LOCAL_WEB_DEFAULT_JSON" "$SERVER:$REMOTE_DIR/default.json"
|
||||
fi
|
||||
log_info "Web 配置上传完成"
|
||||
}
|
||||
|
||||
upload_miniprogram_config() {
|
||||
log_info "上传小程序配置..."
|
||||
scp "$LOCAL_MP_FONTS_JSON" "$SERVER:$REMOTE_MP_ASSETS_DIR/fonts.json"
|
||||
if [ -f "$LOCAL_MP_DEFAULT_JSON" ]; then
|
||||
scp "$LOCAL_MP_DEFAULT_JSON" "$SERVER:$REMOTE_MP_ASSETS_DIR/default.json"
|
||||
else
|
||||
log_warn "未找到小程序 default.json,已跳过: $LOCAL_MP_DEFAULT_JSON"
|
||||
fi
|
||||
log_info "小程序配置上传完成"
|
||||
}
|
||||
|
||||
set_permissions() {
|
||||
log_info "设置文件权限..."
|
||||
ssh $SERVER "chmod -R 755 $REMOTE_DIR"
|
||||
ssh $SERVER "chmod -R 755 $REMOTE_DIR/fonts $REMOTE_MP_ASSETS_DIR"
|
||||
log_info "权限设置完成"
|
||||
}
|
||||
|
||||
verify_deployment() {
|
||||
log_info "验证部署结果..."
|
||||
|
||||
# 检查 fonts.json
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/fonts.json")
|
||||
# 检查小程序 fonts.json
|
||||
MP_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/miniprogram/assets/fonts.json")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
log_info "fonts.json 可访问 ✓"
|
||||
if [ "$MP_HTTP_CODE" = "200" ]; then
|
||||
log_info "小程序 fonts.json 可访问 ✓"
|
||||
else
|
||||
log_error "fonts.json 访问失败 (HTTP $HTTP_CODE)"
|
||||
log_error "小程序 fonts.json 访问失败 (HTTP $MP_HTTP_CODE)"
|
||||
log_warn "请检查 Cloudflare DNS 配置和 Nginx 配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WEB_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/fonts.json")
|
||||
if [ "$WEB_HTTP_CODE" = "200" ]; then
|
||||
log_info "Web fonts.json 可访问 ✓"
|
||||
else
|
||||
log_warn "Web fonts.json 不可访问 (HTTP $WEB_HTTP_CODE)"
|
||||
fi
|
||||
|
||||
# 检查 CORS 头
|
||||
CORS_HEADER=$(curl -s -I "https://fonts.biboer.cn/fonts.json" | grep -i "access-control-allow-origin")
|
||||
CORS_HEADER=$(curl -s -I "https://fonts.biboer.cn/miniprogram/assets/fonts.json" | grep -i "access-control-allow-origin")
|
||||
|
||||
if [ -n "$CORS_HEADER" ]; then
|
||||
log_info "CORS 配置正确 ✓"
|
||||
@@ -123,11 +153,12 @@ show_summary() {
|
||||
echo ""
|
||||
echo "2. 测试字体加载(在小程序开发者工具控制台):"
|
||||
echo " wx.request({"
|
||||
echo " url: 'https://fonts.biboer.cn/fonts.json',"
|
||||
echo " url: 'https://fonts.biboer.cn/miniprogram/assets/fonts.json',"
|
||||
echo " success: (res) => console.log(res.data)"
|
||||
echo " })"
|
||||
echo ""
|
||||
echo "3. 验证 CDN 缓存状态:"
|
||||
echo " curl -I https://fonts.biboer.cn/miniprogram/assets/fonts.json | grep cf-cache-status"
|
||||
echo " curl -I https://fonts.biboer.cn/fonts.json | grep cf-cache-status"
|
||||
echo ""
|
||||
}
|
||||
@@ -151,7 +182,8 @@ main() {
|
||||
check_files
|
||||
create_remote_dirs
|
||||
upload_fonts
|
||||
upload_fonts_json
|
||||
upload_web_config
|
||||
upload_miniprogram_config
|
||||
set_permissions
|
||||
|
||||
# 可选:重启 Nginx(需要 sudo 权限)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
生成字体清单文件
|
||||
扫描 frontend/public/fonts/ 目录下的所有字体文件,同时生成:
|
||||
扫描 fonts/ 目录下的所有字体文件,同时生成:
|
||||
1. frontend/public/fonts.json
|
||||
2. miniprogram/assets/fonts.json
|
||||
3. miniprogram/assets/fonts.js
|
||||
@@ -11,7 +11,7 @@ import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
def scan_fonts(font_dir='frontend/public/fonts'):
|
||||
def scan_fonts(font_dir='fonts'):
|
||||
"""扫描字体目录,返回字体信息列表"""
|
||||
fonts = []
|
||||
font_dir_path = Path(font_dir)
|
||||
@@ -34,19 +34,43 @@ def scan_fonts(font_dir='frontend/public/fonts'):
|
||||
|
||||
relative_path = font_file.relative_to(font_dir_path).as_posix()
|
||||
|
||||
# 生成字体信息
|
||||
# 先生成基础信息,id 在后续统一按序号回填
|
||||
font_info = {
|
||||
'id': f"{category_name}/{font_file.stem}",
|
||||
'id': '',
|
||||
'name': font_file.stem,
|
||||
'filename': font_file.name,
|
||||
'category': category_name,
|
||||
'path': f"/fonts/{relative_path}",
|
||||
'relativePath': relative_path,
|
||||
}
|
||||
|
||||
fonts.append(font_info)
|
||||
|
||||
|
||||
# 统一排序后分配 4 位数字 id(0001、0002...)
|
||||
fonts = sorted(fonts, key=lambda x: (x['category'], x['name'], x['filename']))
|
||||
for index, font in enumerate(fonts, start=1):
|
||||
font['id'] = f"{index:04d}"
|
||||
|
||||
return fonts
|
||||
|
||||
|
||||
def build_manifest(fonts, path_prefix):
|
||||
"""根据路径前缀构建对外清单"""
|
||||
prefix = f"/{str(path_prefix or '').strip('/')}"
|
||||
if prefix == '/':
|
||||
prefix = ''
|
||||
|
||||
manifest = []
|
||||
for font in fonts:
|
||||
manifest.append({
|
||||
'id': font['id'],
|
||||
'name': font['name'],
|
||||
'filename': font['filename'],
|
||||
'category': font['category'],
|
||||
'path': f"{prefix}/{font['relativePath']}",
|
||||
})
|
||||
|
||||
return manifest
|
||||
|
||||
def write_fonts_json(fonts, output_file):
|
||||
"""写入字体清单 JSON 文件"""
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
@@ -66,15 +90,19 @@ def write_fonts_js(fonts, output_file):
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 扫描字体(唯一来源:frontend/public/fonts)
|
||||
fonts = scan_fonts('frontend/public/fonts')
|
||||
# 扫描字体(唯一来源:仓库根目录 fonts/)
|
||||
fonts = scan_fonts('fonts')
|
||||
|
||||
print(f"找到 {len(fonts)} 个字体文件")
|
||||
|
||||
# 同步写入 Web 与小程序清单
|
||||
write_fonts_json(fonts, 'frontend/public/fonts.json')
|
||||
write_fonts_json(fonts, 'miniprogram/assets/fonts.json')
|
||||
write_fonts_js(fonts, 'miniprogram/assets/fonts.js')
|
||||
# Web 清单:统一指向根目录 fonts
|
||||
web_fonts = build_manifest(fonts, '/fonts')
|
||||
write_fonts_json(web_fonts, 'frontend/public/fonts.json')
|
||||
|
||||
# 小程序清单:同样指向根目录 fonts(与 web 共用一份字体目录)
|
||||
miniprogram_fonts = build_manifest(fonts, '/fonts')
|
||||
write_fonts_json(miniprogram_fonts, 'miniprogram/assets/fonts.json')
|
||||
write_fonts_js(miniprogram_fonts, 'miniprogram/assets/fonts.js')
|
||||
|
||||
# 统计信息
|
||||
categories = {}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
"""
|
||||
Font2SVG - fonts.json 生成脚本
|
||||
扫描 frontend/public/fonts/ 目录,生成小程序所需的 fonts.json
|
||||
扫描 fonts/ 目录,生成字体清单
|
||||
URL 格式:https://fonts.biboer.cn/fonts/{category}/{fontname}.ttf
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@ from urllib.parse import quote
|
||||
|
||||
# 配置
|
||||
BASE_URL = "https://fonts.biboer.cn/fonts"
|
||||
FONTS_DIR = Path(__file__).parent.parent / "frontend" / "public" / "fonts"
|
||||
FONTS_DIR = Path(__file__).parent.parent / "fonts"
|
||||
OUTPUT_FILE = Path(__file__).parent.parent / "frontend" / "public" / "fonts.json"
|
||||
|
||||
def scan_fonts(fonts_dir: Path) -> list:
|
||||
@@ -51,9 +51,9 @@ def scan_fonts(fonts_dir: Path) -> list:
|
||||
encoded_filename = quote(font_file.name)
|
||||
url = f"{BASE_URL}/{encoded_category}/{encoded_filename}"
|
||||
|
||||
# 创建字体信息对象
|
||||
# 创建字体信息对象(id 在后续统一按序号回填)
|
||||
font_info = {
|
||||
"id": f"{category}/{font_name}",
|
||||
"id": "",
|
||||
"name": font_name,
|
||||
"category": category,
|
||||
"path": url,
|
||||
@@ -71,7 +71,10 @@ def sort_fonts(fonts: list) -> list:
|
||||
1. 按分类排序
|
||||
2. 同分类内按名称排序
|
||||
"""
|
||||
return sorted(fonts, key=lambda x: (x["category"], x["name"]))
|
||||
sorted_fonts = sorted(fonts, key=lambda x: (x["category"], x["name"]))
|
||||
for index, font in enumerate(sorted_fonts, start=1):
|
||||
font["id"] = f"{index:04d}"
|
||||
return sorted_fonts
|
||||
|
||||
def save_fonts_json(fonts: list, output_file: Path):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user